added migrated 2.x add-ons

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,94 @@
# Cm11a (X10 controller) Binding
The cm11a is a serial computer interface that allows a computer to control attached X10 modules.
## Hardware - cm11a
The cm11a is an older device that communicates over a serial interface.
Most people connect it to a computer using a serial to USB adapter.
This binding has been tested with serial port and a serial to USB adapter.
X10 (and thus the cm11a) supports two types of modules.
The Switch (also called Appliance) module supports being turned on and off.
The Lamp module supports on, off, dim and bright.
In addition to controlling X10 modules the cm11a listens on the powerline and reports to the computer changes made to X10 modules via other controllers.
### Use of serial port
The binding opens the serial port when it starts and keeps it open until the binding is terminated.
If the serial port is disconnected a reconnect will be attempted the next time it is needed.
Therefore, other applications should not attempt to use the port when OpneHAB is running.
However, another program could load macros into the cm11a before openHAB starts.
### cm11a macros
The cm11a is also able to store a schedule and control modules based on that schedule.
That functionality in not currently supported by or used by this binding.
This binding doesn't clear macros from the cm11a so other programs could load macros before openHAB is started.
If you want to do scheduling using openHAB you should be sure there are no macros in the cm11a.
The `heyu clear` command can be used for this purpose.
### X10 powerline monitoring
The cm11a has the ability to capture x10 messages on the powerline.
This binding captures those messages, decodes them and updates the item state.
## Supported things
The binding currently supports the following thing types:
- switch - which supports on and off states
- dimmer - which can be dimmed in addition to turned on or off
## Discovery
Discovery is not supported because that kind of information is not available in the cm11a or the X10 specification.
## Configuration
The cm11a acts as a Bridge to the controlled modules, which are represented as Things.
### Bridge Configuration
The bridge requires the parameter `serialPort` which identifies the serial port the cm11a is connected to.
### Thing Configuration
Each attached thing must specify the `houseUnitCode` set in the device (i.e. A1).
## Channels
| Thing | Channel Type ID | Item Type | Description |
|--------|-----------------|-----------|--------------------|
| switch | switchState | Switch | An On/Off switch |
| dimmer | lightDimmer | Dimmer | A dimmable device |
**Example**
### Things
```perl
Bridge cm11a:cm11a:MyCm11a [ serialPort="COM3" ] {
Thing switch SwitchA1 [ houseUnitCode="A1" ]
Thing dimmer DimmerA2 [ houseUnitCode="A2" ]
}
```
### Items
```java
SwitchA1 "Kitchen Plug" <light> (someGroup) { channel="cm11a:switch:MyCm11a:SwitchA1:switchstatus" }
DimmerA2 "Porch lights" <slider> (someGroup) { channel="cm11a:dimmer:MyCm11a:DimmerA2:lightlevel" }
```
## Known issues
1. When openHAB starts up it doesn't restore the last state of each module. And, the cm11a does not provide a discovery service. Therefore it assumes everything off.
2. The dimmer slider can get out of sync with the actual light because of the way X10 works. On some switches if you turn them on they will go to full bright and some switches will return to the previous dim level.
## References
1. [CM11A (X10) Protocol Document](https://wanderingsamurai.net/electronics/cm11a-x10-protocol-document)
2. [Heyu - control software for the cm11a](https://www.heyu.org/)
3. cm11a Controllers are available for purchase from several sites on the internet

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.cm11a</artifactId>
<name>openHAB Add-ons :: Bundles :: CM11A Binding</name>
<properties>
<bnd.importpackage>gnu.io;version="[3.12,6)"</bnd.importpackage>
</properties>
</project>

View File

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

View File

@@ -0,0 +1,45 @@
/**
* 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.cm11a.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link CM11ABinding} class defines common constants, which are
* used across the whole binding.
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public class CM11ABindingConstants {
public static final String BINDING_ID = "cm11a";
/**
* Bridge Type UIDs
*/
public static final ThingTypeUID THING_TYPE_CM11A = new ThingTypeUID(BINDING_ID, "cm11a");
/**
* List of all Thing Type UIDs
*/
public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(BINDING_ID, "dimmer");
public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch");
/**
* List of all Channel ids
*/
public static final String CHANNEL_LIGHTLEVEL = "lightlevel";
public static final String CHANNEL_SWITCH = "switchstatus";
}

View File

@@ -0,0 +1,69 @@
/**
* 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.cm11a.internal;
import static org.openhab.binding.cm11a.internal.CM11ABindingConstants.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.openhab.binding.cm11a.internal.handler.Cm11aApplianceHandler;
import org.openhab.binding.cm11a.internal.handler.Cm11aBridgeHandler;
import org.openhab.binding.cm11a.internal.handler.Cm11aLampHandler;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Cm11aHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Bob Raker - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.cm11a")
public class Cm11aHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(Cm11aHandlerFactory.class);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(THING_TYPE_SWITCH, THING_TYPE_DIMMER)));
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return THING_TYPE_CM11A.equals(thingTypeUID) || SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
logger.trace("**** Cm11AHandlerFactory.createHandler for {}", thingTypeUID.getAsString());
if (thingTypeUID.equals(THING_TYPE_CM11A)) {
return new Cm11aBridgeHandler((Bridge) thing);
} else if (thingTypeUID.equals(THING_TYPE_SWITCH)) {
return new Cm11aApplianceHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_DIMMER)) {
return new Cm11aLampHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,27 @@
/**
* 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.cm11a.internal;
/**
* Exception for invalid X10 House / Unit code
*
* @author Bob Raker - Initial contribution
*/
public class InvalidAddressException extends Exception {
private static final long serialVersionUID = -1049253542819790311L;
public InvalidAddressException(String string) {
super(string);
}
}

View File

@@ -0,0 +1,808 @@
/**
* 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.cm11a.internal;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TooManyListenersException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import org.openhab.binding.cm11a.internal.handler.Cm11aAbstractHandler;
import org.openhab.binding.cm11a.internal.handler.Cm11aBridgeHandler;
import org.openhab.binding.cm11a.internal.handler.ReceivedDataListener;
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.SerialPortEvent;
import gnu.io.SerialPortEventListener;
import gnu.io.UnsupportedCommOperationException;
/**
* Driver for the CM11 X10 interface.
*
*
* @author Anthony Green - Original code
* @author Bob Raker - updates to setClock code, adapted code for use in openHAB2
* @see <a href="http://www.heyu.org/docs/protocol.txt">CM11 Protocol specification</a>
* @see <a href="http://www.rxtx.org">RXTX Serial API for Java</a>
*/
public class X10Interface extends Thread implements SerialPortEventListener {
private final Logger logger = LoggerFactory.getLogger(X10Interface.class);
// X10 Function codes
public static final int FUNC_ALL_UNITS_OFF = 0x0;
public static final int FUNC_ALL_LIGHTS_ON = 0x1;
public static final int FUNC_ON = 0x2;
public static final int FUNC_OFF = 0x3;
public static final int FUNC_DIM = 0x4;
public static final int FUNC_BRIGHT = 0x5;
public static final int FUNC_ALL_LIGHTS_OFF = 0x6;
public static final int FUNC_EXTENDED = 0x7;
public static final int FUNC_HAIL_REQ = 0x8;
public static final int FUNC_HAIL_ACK = 0x9;
public static final int FUNC_PRESET_DIM_1 = 0xA;
public static final int FUNC_PRESET_DIM_2 = 0xB;
public static final int FUNC_EXT_DATA_TRANSFER = 0xC;
public static final int FUNC_STATUS_ON = 0xD;
public static final int FUNC_STATUS_OFF = 0xE;
public static final int FUNC_STATUS_REQ = 0xF;
// Definitions for the header:code bits
/**
* Bit mask for the bit that is always set in a header:code
*/
static final int HEAD = 0x04;
/**
* Bit mask for Function/Address bit of header:code.
*/
static final int HEAD_FUNC = 0x02;
/**
* Bit mask for standard/extended transmission bit of header:code.
*/
static final int HEAD_EXTENDED = 0x01;
/**
* Byte sent from PC to Interface to acknowledge the receipt of a correct checksum.
* If the checksum was incorrect, the PC should retransmit.
*/
static final int CHECKSUM_ACK = 0x00;
/**
* Byte send from Interface to PC to indicate it has sent the desired data over the
* X10/power lines.
*/
static final int IF_READY = 0x55;
/**
* Byte sent from Interface to PC to request that its clock is set.
* Interface will send this to the PC repeatedly after a power-failure and will not respond to commands
* until its clock has been set.
*/
static final int CLOCK_SET_REQ = 0xA5;
/**
* Byte sent from PC to interface to start a transmission that sets the interface clock.
*/
static final int CLOCK_SET_HEAD = 0x9B;
/**
* Byte sent from interface to PC to indicate that it has X10 data pending transmission to the PC.
*/
static final int DATA_READY_REQ = 0x5a;
static final int DATA_READY_HEAD = 0xc3;
/**
* This command is purely intended for the CP10.
* The power-strip contains an input filter and electrical surge protection
* that is monitored by the microcontroller. If this protection should
* become compromised (i.e. resulting from a lightening strike) the
* interface will attempt to wake the computer with a 'filter-fail poll'.
*/
static final int INPUT_FILTER_FAIL_REQ = 0xf3;
static final int INPUT_FILTER_FAIL_HEAD = 0xf3;
/**
* Byte sent from PC to interface to enable interface feature that brings serial port RI high when
* data arrives to send to PC
*/
static final int RI_ENABLE = 0xeb;
static final int RI_DISABLE = 0x55;
/**
* THe house code to be monitored. Not sure what this means, but it is part of the clock set instruction.
* For the moment hardcoded here to be House 'E'.
*/
static final int MONITORED_HOUSE_CODE = 0x10;
static final Map<Character, Integer> HOUSE_CODES;
static final Map<Integer, Integer> DEVICE_CODES;
static {
HashMap<Character, Integer> houseCodes = new HashMap<>(16);
houseCodes.put('A', 0x60);
houseCodes.put('B', 0xE0);
houseCodes.put('C', 0x20);
houseCodes.put('D', 0xA0);
houseCodes.put('E', 0x10);
houseCodes.put('F', 0x90);
houseCodes.put('G', 0x50);
houseCodes.put('H', 0xD0);
houseCodes.put('I', 0x70);
houseCodes.put('J', 0xF0);
houseCodes.put('K', 0x30);
houseCodes.put('L', 0xB0);
houseCodes.put('M', 0x00);
houseCodes.put('N', 0x80);
houseCodes.put('O', 0x40);
houseCodes.put('P', 0xC0);
HOUSE_CODES = Collections.unmodifiableMap(houseCodes);
HashMap<Integer, Integer> deviceCodes = new HashMap<>(16);
deviceCodes.put(1, 0x06);
deviceCodes.put(2, 0x0E);
deviceCodes.put(3, 0x02);
deviceCodes.put(4, 0x0A);
deviceCodes.put(5, 0x01);
deviceCodes.put(6, 0x09);
deviceCodes.put(7, 0x05);
deviceCodes.put(8, 0x0D);
deviceCodes.put(9, 0x07);
deviceCodes.put(10, 0x0F);
deviceCodes.put(11, 0x03);
deviceCodes.put(12, 0x0B);
deviceCodes.put(13, 0x00);
deviceCodes.put(14, 0x08);
deviceCodes.put(15, 0x04);
deviceCodes.put(16, 0x0C);
DEVICE_CODES = Collections.unmodifiableMap(deviceCodes);
}
// Constants that control the interaction with the hardware
static final int IO_PORT_OPEN_TIMEOUT = 5000;
/**
* Number of CM11a dim increments for dimable devices
*/
static final int CM11A_DIM_INCREMENTS = 22;
/**
* How long to wait between attempts to reconnect to the interface. (ms)
*/
static final int IO_RECONNECT_INTERVAL = 5000;
/**
* Maximum number of times to retry sending a bit of data when checksum errors occur.
*/
static final int IO_MAX_SEND_RETRY_COUNT = 5;
static final int SERIAL_TIMEOUT_MSEC = 4000;
// Hardware IO attributes
protected CommPortIdentifier portId;
protected SerialPort serialPort;
protected boolean connected = false;
protected DataOutputStream serialOutput;
protected OutputStream serialOutputStr;
protected DataInputStream serialInput;
protected InputStream serialInputStr;
// Listeners that are to be notified when new data is received from the cm11a
private List<ReceivedDataListener> receiveListeners = new ArrayList<>();
/**
* Flag to indicate that background thread should be killed. Used to deactivate plugin.
*/
protected volatile boolean killThread = false;
// Scheduling attributes
/**
* Queue of as-yet un-actioned requests.
*/
protected BlockingQueue<Cm11aAbstractHandler> deviceUpdateQueue = new ArrayBlockingQueue<>(256);
// Need to keep last addresses found for data that comes in over the serial interface because if the incoming
// command is a dim or bright the address isn't included. In addition some controllers will send the address
// in one message and the function in a second one
private List<String> lastAddresses;
/**
* Need to have access to BridgeHandler so it's status can be updated.
*/
private Cm11aBridgeHandler bridgeHandler;
/**
*
* @param serialPort serial port device. e.g. /dev/ttyS0
* @throws NoSuchPortException
*
*/
public X10Interface(String serialPort, Cm11aBridgeHandler bridgeHandler) throws NoSuchPortException {
super();
logger.trace("**** Constructing X10Interface for serial port: {} *******", serialPort);
portId = CommPortIdentifier.getPortIdentifier(serialPort);
this.bridgeHandler = bridgeHandler;
}
/**
* Establishes a serial connection to the hardware, if one is not already established.
*/
protected boolean connect() {
if (!connected) {
if (serialPort != null) {
logger.trace("Closing stale serialPort object before reconnecting");
serialPort.close();
}
logger.debug("Connecting to X10 hardware on serial port: {}", portId.getName());
try {
serialPort = portId.open("Openhab CM11A Binding", IO_PORT_OPEN_TIMEOUT);
serialPort.setSerialPortParams(4800, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
serialPort.disableReceiveTimeout();
serialPort.enableReceiveThreshold(1);
serialOutputStr = serialPort.getOutputStream();
serialOutput = new DataOutputStream(serialOutputStr);
serialInputStr = serialPort.getInputStream();
serialInput = new DataInputStream(serialInputStr);
serialPort.addEventListener(this);
connected = true;
serialPort.notifyOnDataAvailable(true);
serialPort.notifyOnRingIndicator(true);
// Bob Raker - add serial timeout to prevent possible hang conditions
serialPort.enableReceiveTimeout(SERIAL_TIMEOUT_MSEC);
if (!serialPort.isReceiveTimeoutEnabled()) {
logger.info("Serial receive timeout not supported by this driver.");
}
bridgeHandler.changeBridgeStatusToUp();
} catch (PortInUseException e) {
String message = String.format("Serial port %s is in use by another application (%s)", portId.getName(),
e.currentOwner);
logger.warn("{}", message);
bridgeHandler.changeBridgeStatusToDown(message);
} catch (UnsupportedCommOperationException e) {
logger.warn("Serial port {} doesn't support the required baud/parity/stopbits or Timeout",
portId.getName());
} catch (IOException e) {
logger.warn("IO Problem with serial port {} . {}", portId.getName(), e.getMessage());
} catch (TooManyListenersException e) {
logger.warn(
"TooManyListeners error when trying to connect to serial port. Interface is unlikely to work, raise a bug report.",
e);
bridgeHandler.changeBridgeStatusToDown("ToManyListenersException");
}
} else {
logger.trace("Already connected to hardware, skipping reconnection.");
}
return connected;
}
/**
* Transmits a standard (non-extended) X10 function.
*
* @param address
* @param function
* @param dims 0-22, number of dims/brights to send.
* @return true if update was successful
* @throws InvalidAddressException
* @throws IOException
*/
public boolean sendFunction(String address, int function, int dims) throws InvalidAddressException, IOException {
boolean success = false;
if (!validateAddress(address)) {
throw new InvalidAddressException("Address " + address + " is not a valid X10 address");
}
int houseCode = HOUSE_CODES.get(address.charAt(0));
int deviceCode = DEVICE_CODES.get(Integer.parseInt(address.substring(1)));
int[] data = new int[2];
if (connect()) {
synchronized (serialPort) {
logger.trace("Sending a standard X10 function to device: {}", address);
// First send address
data[0] = HEAD;
data[1] = houseCode | deviceCode;
sendData(data);
// Now send function call
data[0] = HEAD | HEAD_FUNC | (dims << 3);
data[1] = houseCode | function;
sendData(data);
success = true;
}
}
return success;
}
/**
* Validates that the given string is a valid X10 address. Returns true if this is the case.
*
* @param address
* @return
*/
public static boolean validateAddress(String address) {
return (!(address.length() < 2 || address.length() > 3
|| !HOUSE_CODES.containsKey(new Character(address.charAt(0)))
|| !DEVICE_CODES.containsKey(Integer.parseInt(address.substring(1)))));
}
/**
* Queues a standard (non-extended) X10 function for transmission.
*
* @param address
* @param function
* @return true for success
* @throws InvalidAddressException
* @throws IOException
*/
public boolean sendFunction(String address, int function) throws InvalidAddressException, IOException {
return sendFunction(address, function, 0);
}
/**
* Add specified device into the queue for hardware updates.
*
* <p>
* If device is already queued, it will be removed from queue and moved to the end.
* </p>
*
* @param device
*/
public void scheduleHWUpdate(Cm11aAbstractHandler device) {
deviceUpdateQueue.remove(device);
if (!deviceUpdateQueue.offer(device)) {
logger.warn(
"X10 function call queue full. Too many outstanding commands. This command will be discarded");
}
logger.debug("Added item to cm11a queue for: {}", device.getThing().getLabel());
}
/**
* Sends data to the hardware and handles the checksuming and retry process.
*
* <p>
* When applicable, method blocks until the data has actually been sent over the powerlines using X10
* </p>
*
* @param data Data to be sent.
* @throws IOException
*/
protected void sendData(int[] data) throws IOException {
int calcChecksum = 0;
int checksumResponse = -1;
// Calculate expected checksum:
for (int i = 0; i < data.length; i++) {
// Note that ints are signed in Java, but the checksum uses unsigned ints.
// Hence some jiggery pokery to get an unsigned int from the data int.
calcChecksum = (calcChecksum + (0x000000FF & (data[i]))) & 0x000000FF;
logger.trace("Checksum calc: int {} = {}", i, Integer.toHexString(data[i]));
}
if (connect()) {
synchronized (serialPort) {
long startTime = System.currentTimeMillis(); // do some timing analysis
// Stop background data listener as we want to have a dialogue with the interface here.
serialPort.notifyOnDataAvailable(false);
// Need to catch possible EOF exception if there is a timeout
try {
int retryCount = 0;
while (checksumResponse != calcChecksum) {
retryCount++;
for (int i = 0; i < data.length; i++) {
serialOutput.write(data[i]);
serialOutput.flush();
}
long sendTime = System.currentTimeMillis();
logger.trace("Sent the following data out the serial port in {} msec, {}",
(sendTime - startTime), Arrays.toString(data));
checksumResponse = serialInput.readUnsignedByte();
logger.trace("Attempted to send data, try number: {} Checksum expected: {} received: {}",
retryCount, Integer.toHexString(calcChecksum), Integer.toHexString(checksumResponse));
long ckSumTime = System.currentTimeMillis();
logger.trace("Received serial port check sum in {} msec", (ckSumTime - sendTime));
if (checksumResponse != calcChecksum) {
// On initial device power up, nothing works until we set the clock. Check to see if the
// unexpected data was actually a request from interface to PC.
processRequestFromIFace(checksumResponse);
if (retryCount > IO_MAX_SEND_RETRY_COUNT) {
logger.warn("Failed to send data to X10 hardware due to too many checksum failures");
serialPort.notifyOnDataAvailable(true);
throw new IOException("Max retries exceeded");
}
}
}
logger.trace(
"Data transmission to interface was successful, sending ACK. X10 transmission over powerline will now commence.");
long ackTime = System.currentTimeMillis();
serialOutput.write(CHECKSUM_ACK);
serialOutput.flush();
int response = serialInput.readUnsignedByte();
if (response == IF_READY) {
long cmpltdTime = System.currentTimeMillis();
logger.trace("Serial port X10 ACK completed in {} msec, TOTAL X10 TRANSMISSION TIME in {} ms",
(cmpltdTime - ackTime), (cmpltdTime - startTime));
} else {
logger.warn("Expected IF_READY ({}) response from hardware but received: {} instead",
Integer.toHexString(IF_READY & 0x00000FF), Integer.toHexString(response & 0x00000FF));
}
} catch (EOFException ex) {
logger.warn(
"Received EOF exception while sending X10 command after {} ms. Make sure the cm11a is connected to the serial port",
(System.currentTimeMillis() - startTime));
}
serialPort.notifyOnDataAvailable(true);
}
}
}
@Override
public void run() {
logger.trace("Starting X10Interface background thread...");
// call connect so any X10 events on the powerline will be picked up by the serialEvent listener
connect();
while (!killThread) {
try {
Cm11aAbstractHandler nextModule;
logger.trace("Getting next module to be updated");
nextModule = deviceUpdateQueue.take();
logger.trace("Got a device. Going to run it.");
// Keep retrying to update this device until it is successful.
updateHardware(nextModule);
} catch (InterruptedException e1) {
Thread.currentThread().interrupt(); // See https://www.ibm.com/developerworks/library/j-jtp05236/ for
// discussion of resetting interrupt flag
logger.warn("Unexpected interrupt on X10 scheduling thread. Ignoring and continuing anyway...");
}
}
logger.trace("Stopping background thread...");
this.notifyAll();
}
/**
* Perform Hardware update. Keep trying until it is successful
*
* @param nextModule - Next module that needs to be updated
* @throws InterruptedException
*/
private void updateHardware(Cm11aAbstractHandler nextModule) throws InterruptedException {
boolean success = false;
while (!success) {
try {
if (connect()) {
nextModule.updateHardware(this);
success = true;
} else {
Thread.sleep(IO_RECONNECT_INTERVAL);
}
} catch (IOException e) {
connected = false;
String message = "IO Exception when updating module hardware. Will retry shortly";
logger.warn(message, e);
bridgeHandler.changeBridgeStatusToDown(message);
Thread.sleep(IO_RECONNECT_INTERVAL);
} catch (InvalidAddressException e) {
logger.warn("Attempted to send an X10 Function call with invalid address. Ignoring this.");
success = true; // Pretend this was successful as retrying will be pointless.
}
}
}
@Override
public void serialEvent(SerialPortEvent event) {
try {
if (event.getEventType() == SerialPortEvent.DATA_AVAILABLE || event.getEventType() == SerialPortEvent.RI) {
synchronized (serialPort) {
logger.trace("Serial port data available or RI indicator event received");
while (serialPort.isRI() && serialInput.available() <= 0) {
logger.trace("Ring indicator is High but there is no data. Waiting for data...");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
logger.debug("Interrupted while sleeping", e);
}
}
while (serialInput.available() > 0) {
logger.trace("{} bytes of data available to be read", serialInput.available());
int readint = serialInput.read();
processRequestFromIFace(readint);
// Wait a while before rechecking to give interface time to switch off Ring Indicator.
while (serialPort.isRI() && serialInput.available() <= 0) {
logger.trace("Ring indicator is High but there is no data. Waiting for data...");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// Reset the flag and continue
}
}
}
logger.trace(
"Reading data from interface complete. Ring Indicator has cleared and no data is left to read.");
}
}
} catch (IOException e) {
logger.warn("IO Exception in serial port handler callback: {}", e.getMessage());
}
}
/**
* Processes a request made from the interface to the PC. Only handles requests initiated by the interface,
* not those that form part of a conversation triggered by the PC.
*
* @param readint
* @throws IOException
*/
protected void processRequestFromIFace(int readint) throws IOException {
switch (readint) {
case CLOCK_SET_REQ:
setClock();
break;
case DATA_READY_REQ:
receiveCommandData();
break;
case INPUT_FILTER_FAIL_REQ:
serialOutput.write(DATA_READY_HEAD);
logger.warn(
"X10 Interface has indicated that the filter and/or surge protection in the device has failed.");
break;
default:
logger.warn("Unexpected data received from X10 interface: {}", Integer.toHexString(readint));
}
}
/**
* Sets the internal clock on the X10 interface
*
* @throws IOException
*/
private void setClock() throws IOException {
logger.debug("Setting clock in X10 interface");
Calendar cal = Calendar.getInstance();
int[] clockData = new int[7];
clockData[0] = CLOCK_SET_HEAD;
clockData[1] = cal.get(Calendar.SECOND);
clockData[2] = cal.get(Calendar.MINUTE) + (cal.get(Calendar.HOUR) % 2) * 60;
clockData[3] = cal.get(Calendar.HOUR_OF_DAY) / 2;
// Note: Calendar.DAY_OF_YEAR starts at 1 but the C language tm_yday starts at 0. Need 0 based DAY_OF_YEAR for
// cm11a
clockData[4] = (cal.get(Calendar.DAY_OF_YEAR) - 1) % 256;
// Note: Calendar.DAY_OF_WEEK is 1 based and need 0 based (i.e. Sunday = 1
clockData[5] = (((cal.get(Calendar.DAY_OF_YEAR - 1)) / 256) << 7)
| (0x01 << (cal.get(Calendar.DAY_OF_WEEK) - 1));
// There are other flags in this final byte to do with clearing timer, battery timer and monitored status.
// I've no idea what they are, so have left them unset.
clockData[6] = MONITORED_HOUSE_CODE;
sendData(clockData);
}
/**
* Process data that the X10 interface is waiting to send to the PC
*
* @throws IOException
*/
private void receiveCommandData() throws IOException {
logger.debug("Receiving X10 data from interface");
// Send acknowledgement to interface
serialOutput.write(DATA_READY_HEAD);
// Read the buffer size
// There might be several DATA_READY_REQ bytes which need to be ignored
int length = 0;
do {
length = serialInput.read();
} while (length == DATA_READY_REQ || length > 8); // if the length is >8 this isn't a valid length so ignore
// Next read the Function / Address Mask byte
int mask = serialInput.read();
length--; // This read counts as part of the length
logger.debug("Receiving [{}] bytes, Addr/Func mask: {}", length, Integer.toBinaryString(mask));
// It is possible for the cm11a buffer to be empty in which case no processing can take place
if (length > 0) {
int[] data = new int[length];
for (int i = 0; i < length; i++) {
int recvByte = serialInputStr.read();
data[i] = recvByte;
logger.debug(" Received X10 data [{}]: {}", i, Integer.toHexString(recvByte));
}
processCommandData(mask, data);
} else {
logger.warn("cm11a buffer was overrun. Any pending commands will be ignorred until the buffer clears.");
}
}
/**
* Process raw data received from the interface and convert into human readable data
*
* @param mask Function / Address Mask. Each bit corresponds to the contents of a data. A 0 bit
* corresponds to an address. A 1 bit corresponds to a function.
* @param data
*/
private void processCommandData(int mask, int[] data) {
int localMask = mask;
X10ReceivedData.X10COMMAND command = X10ReceivedData.X10COMMAND.UNDEF; // Just set it to something
List<String> addresses = new ArrayList<>();
int dims = 0;
List<X10ReceivedData> rcvData = new ArrayList<>();
for (int i = 0; i < data.length; i++) {
int d = data[i];
int dataType = localMask & 0x01;
if (dataType == 0) {
// The data byte is an address
int houseIndex = (d >> 4) & 0x0f;
int unitIndex = d & 0x0f;
addresses.add(Character.toString(X10ReceivedData.HOUSE_CODE[houseIndex])
+ Integer.toString(X10ReceivedData.UNIT_CODE[unitIndex]));
} else {
// The data byte is a function
command = X10ReceivedData.COMMAND_MAP.get(d & 0x0f);
if (command == null) {
command = X10ReceivedData.X10COMMAND.UNDEF;
} else if (command == X10ReceivedData.X10COMMAND.BRIGHT || command == X10ReceivedData.X10COMMAND.DIM) {
// Have to read one more byte which is the dim level
if (i < data.length) {
dims = data[++i];
// This dims is a number between 0 and 210 and is the CHANGE in brightness level
// The interface transmits 1 to 22 dims to go from full bright to full dim
// therefore this need to be converted to a number between 1 and 22. Always want to dim or
// brighten one increment. The conversion is therefore:
dims = (dims * CM11A_DIM_INCREMENTS) / 210;
dims = dims > 0 ? dims : 1;
}
// Also in this case no addresses would have been sent so use the saved addresses
// If there were no previous commands that specified an address then lastAddress will be null. In
// this case we can't do anything with this dim request.
if (lastAddresses == null) {
logger.info(
"cm11a received a dim command but there is no prior commands that included an address.");
continue;
}
addresses = lastAddresses;
} else if (command == X10ReceivedData.X10COMMAND.ALL_LIGHTS_OFF
|| command == X10ReceivedData.X10COMMAND.ALL_LIGHTS_ON
|| command == X10ReceivedData.X10COMMAND.ALL_UNITS_OFF) {
logger.warn("cm11a received the command: {}. This command is ignored by this binding.", command);
continue;
} else {
// A valid command must have been received.
// As indicated above, some controllers send the address in one transmission and the function in a
// second transaction.
// Check if we have gotten an address in the transmission and if not use lastAddresses if they
// are not null
if (addresses.isEmpty()) {
// No addresses were sent in this transmission, see if we can use lastAddresses
if (lastAddresses == null) {
logger.info("cm11a received a command but the transmission didn't include an address.");
continue;
} else {
addresses = lastAddresses;
logger.info(
"cm11a received a command without any addresses. Addresses from a prior reception are available and will be used.");
}
}
}
// Every time we get a function it is the end of the transmission and we can bundle the data into an
// X10ReceivedData object.
X10ReceivedData rd = new X10ReceivedData(addresses.toArray(new String[0]), command, dims);
rcvData.add(rd);
logger.debug("cm11a: Added received data to queue: {}", rd.toString());
// reset the data objects for the next thing in the buffer, if any
command = X10ReceivedData.X10COMMAND.UNDEF;
// Collections.copy(lastAddresses, addresses);
lastAddresses = addresses;
addresses = new ArrayList<>();
dims = 0;
}
localMask = localMask >> 1;
}
// Done processing buffer from cm11a. Notify interested parties about the data
for (X10ReceivedData rd : rcvData) {
logger.debug("cm11a: Converted received data to human form: {}", rd.toString());
notifyReceiveListeners(rd);
}
}
/**
* Called by classes that want to be notified when data has been received from the cm11a
*
* @param listener The class to be called
*/
public void addReceivedDataListener(ReceivedDataListener listener) {
receiveListeners.add(listener);
}
/**
* Called to notify classes that data has been received
*
* @param rcv
*/
private void notifyReceiveListeners(X10ReceivedData rcv) {
for (ReceivedDataListener rl : receiveListeners) {
rl.receivedX10Data(rcv);
}
}
/**
* Disconnect from hardware
*/
public void disconnect() {
// Kill the thread that performs the hardware updates
killThread = true;
if (serialInput != null) {
try {
serialInput.close();
} catch (IOException e) {
// nothing to do if there is an issue closing the stream.
}
}
if (serialOutput != null) {
try {
serialOutput.close();
} catch (IOException e) {
// nothing to do if there is an issue closing the stream.
}
}
if (serialPort != null) {
serialPort.close();
}
}
}

View File

@@ -0,0 +1,153 @@
/**
* 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.cm11a.internal;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Container for data received from the powerline by the cm11a interface. When data is received one of more of these
* objects is created and then passed to interested objects.
*
* @author Bob Raker - Initial contribution
*/
public class X10ReceivedData {
/**
* All of the possible X10 commands
*
*/
public enum X10COMMAND {
ALL_UNITS_OFF,
ALL_LIGHTS_ON,
ON,
OFF,
DIM,
BRIGHT,
ALL_LIGHTS_OFF,
EXTENDED_CODE,
HAIL_REQ,
HAIL_ACK,
PRESET_DIM_1,
PRESET_DIM_2,
EXTD_DATA_XFER,
STATUS_ON,
STATUS_OFF,
STATUS_REQ,
UNDEF // If no match, which shouldn't happen
}
/**
* Used to decode the function bits received from the cm11a into an X10 function code
*
*/
protected static final Map<Integer, X10COMMAND> COMMAND_MAP;
static {
Map<Integer, X10COMMAND> tempMap = new HashMap<>();
tempMap.put(0, X10COMMAND.ALL_UNITS_OFF);
tempMap.put(1, X10COMMAND.ALL_LIGHTS_ON);
tempMap.put(2, X10COMMAND.ON);
tempMap.put(3, X10COMMAND.OFF);
tempMap.put(4, X10COMMAND.DIM);
tempMap.put(5, X10COMMAND.BRIGHT);
tempMap.put(6, X10COMMAND.ALL_LIGHTS_OFF);
tempMap.put(7, X10COMMAND.EXTENDED_CODE);
tempMap.put(8, X10COMMAND.HAIL_REQ);
tempMap.put(9, X10COMMAND.HAIL_ACK);
tempMap.put(10, X10COMMAND.PRESET_DIM_1);
tempMap.put(11, X10COMMAND.PRESET_DIM_2);
tempMap.put(12, X10COMMAND.EXTD_DATA_XFER);
tempMap.put(13, X10COMMAND.STATUS_ON);
tempMap.put(14, X10COMMAND.STATUS_OFF);
tempMap.put(15, X10COMMAND.STATUS_REQ);
COMMAND_MAP = Collections.unmodifiableMap(tempMap);
}
/**
* Lookup table to convert House code received from the cm11a into an X10 house code
*/
public static final char HOUSE_CODE[] = new char[] { 'M', 'E', 'C', 'K', 'O', 'G', 'A', 'I', 'N', 'F', 'D', 'L',
'P', 'H', 'B', 'J' };
/**
* Lookup table to convert Unit code received from the cm11a into an X10 unit code
*/
public static final byte UNIT_CODE[] = new byte[] { 13, 5, 3, 11, 15, 7, 1, 9, 14, 6, 4, 12, 16, 8, 2, 10 };
private String[] addr;
private X10COMMAND cmd;
private int dims;
/**
* Constructor
*/
public X10ReceivedData(String[] addr, X10COMMAND cmd, int dims) {
this.addr = addr;
this.cmd = cmd;
this.dims = dims;
}
public String[] getAddr() {
return addr;
}
public X10COMMAND getCmd() {
return cmd;
}
public int getDims() {
return dims;
}
@Override
public String toString() {
return "X10ReceivedData [addr=" + Arrays.toString(addr) + ", cmd=" + cmd + ", dims=" + dims + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(addr);
result = prime * result + ((cmd == null) ? 0 : cmd.hashCode());
result = prime * result + dims;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
X10ReceivedData other = (X10ReceivedData) obj;
if (!Arrays.equals(addr, other.addr)) {
return false;
}
if (cmd != other.cmd) {
return false;
}
if (dims != other.dims) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.cm11a.internal.config;
/**
* Configuration constants for Cm11A
*
* @author Bob Raker - Initial contribution
*
*/
public class Cm11aConfig {
/**
* Serial port of the CM11A device.
*/
public String serialPort = null;
/**
* refresh rate - I don't think we will need this
*/
public int refresh = 60;
}

View File

@@ -0,0 +1,183 @@
/**
* 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.cm11a.internal.handler;
import java.io.IOException;
import org.openhab.binding.cm11a.internal.InvalidAddressException;
import org.openhab.binding.cm11a.internal.X10Interface;
import org.openhab.core.config.core.Configuration;
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.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.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is an abstract base class for the "Thing" handlers (i.e. Cm11aApplianceHandler and Cm11aLampHandler).
* It is not used by the Bridge handler (Cm11aHandler)
*
* @author Bob Raker - Initial contribution
*
*/
public abstract class Cm11aAbstractHandler extends BaseThingHandler {
/**
* The House and Unit codes set on the module, i.e. A1, J14
*/
protected String houseUnitCode;
/**
* The X10 function
*/
protected int x10Function;
/**
* The channel ID
*/
protected ChannelUID channelUID;
/**
* The current State of the device
*/
protected State currentState;
/**
* Number of CM11a dim increments for dimable devices
*/
static final int X10_DIM_INCREMENTS = 22;
static final String HOUSE_UNIT_CODE = "houseUnitCode";
static final String NO_BRIDGE_ERROR = "No bridge found using house unit code ";
private final Logger logger = LoggerFactory.getLogger(Cm11aAbstractHandler.class);
/**
* The construction
*
* @param thing The "Thing" to be handled
*/
public Cm11aAbstractHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
Configuration config = thing.getConfiguration();
houseUnitCode = (String) config.get(HOUSE_UNIT_CODE);
Bridge bridge = getBridge();
if (bridge == null) {
logger.warn("{}", NO_BRIDGE_ERROR + houseUnitCode);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, NO_BRIDGE_ERROR + houseUnitCode);
return;
}
if (ThingStatus.ONLINE.equals(bridge.getStatus())) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
public void dispose() {
houseUnitCode = null;
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
logger.debug("CM11A status changed to {}.", bridgeStatusInfo.getStatus());
if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
logger.debug("CM11A is not online. Bridge status: {}", bridgeStatusInfo.getStatus());
return;
}
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
if (houseUnitCode.length() > 0) {
// The config must be present and was set during initialization
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
}
}
}
/**
* Will be called by the X10Interface when it is ready for this X10 device to use the X10 bus.
* Child classes should override this method with the specific process necessary to update the
* hardware with the latest data.
*
* <p>
* Warning: This will be called in a different thread. It must be thread safe.
* </p>
*
* <p>
* Retries in the event of interface problems will be handled by the X10Interface. If a comms
* problem occurs and the method throws an exception, this device will be rescheduled again later.
* </p>
*/
public abstract void updateHardware(X10Interface x10Interface) throws IOException, InvalidAddressException;
public State getCurrentState() {
return currentState;
}
public void setCurrentState(State currentState) {
this.currentState = currentState;
}
/**
* Subtract the specified number of X10 "dims" from the current state
*
* @param dims The number of dims to remove
* @return The updated current state
*/
public State addDimsToCurrentState(int dims) {
if (!(currentState instanceof PercentType)) {
if (currentState instanceof OnOffType && currentState == OnOffType.ON) {
currentState = PercentType.HUNDRED;
} else {
currentState = PercentType.ZERO;
}
}
// The current state is stored in a PercentType object and therefore needs to be converted to an incremental
// percent
int curPercent = ((PercentType) currentState).intValue();
// dims is a number between 0 and 22 which represents the full range of cm11a
int dimsPercent = (dims * 100) / X10_DIM_INCREMENTS;
int newPercent = curPercent - dimsPercent;
newPercent = Math.max(newPercent, 0);
newPercent = Math.min(newPercent, 100);
currentState = new PercentType(newPercent);
return currentState;
}
/**
* Add the specified number of X10 "dims" from the current state
*
* @param dims The number of dims to remove
* @return The updated current state
*/
public State addBrightsToCurrentState(int dims) {
return addDimsToCurrentState(-dims);
}
}

View File

@@ -0,0 +1,96 @@
/**
* 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.cm11a.internal.handler;
import java.io.IOException;
import org.openhab.binding.cm11a.internal.InvalidAddressException;
import org.openhab.binding.cm11a.internal.X10Interface;
import org.openhab.core.library.types.OnOffType;
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.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler for Appliance (also called Switch) modules. These modules only support ON and OFF states
*
* @author Bob Raker - Initial contribution
*
*/
public class Cm11aApplianceHandler extends Cm11aAbstractHandler {
private final Logger logger = LoggerFactory.getLogger(Cm11aApplianceHandler.class);
private State desiredState = UnDefType.UNDEF;
public Cm11aApplianceHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("**** Cm11aApplianceHandler handleCommand command = {}, channelUID = {}", command,
channelUID.getAsString());
x10Function = 0;
Bridge bridge = getBridge();
if (bridge == null) {
logger.debug("Unable to handle command. Bridge is null.");
return;
}
this.channelUID = channelUID;
Cm11aBridgeHandler cm11aHandler = (Cm11aBridgeHandler) bridge.getHandler();
if (cm11aHandler != null && cm11aHandler.getThing().getStatus().equals(ThingStatus.ONLINE)) {
if (command == OnOffType.ON) {
x10Function = X10Interface.FUNC_ON;
desiredState = OnOffType.ON;
} else if (command == OnOffType.OFF) {
x10Function = X10Interface.FUNC_OFF;
desiredState = OnOffType.OFF;
} else if (command instanceof RefreshType) {
x10Function = X10Interface.FUNC_OFF;
desiredState = OnOffType.OFF;
logger.info("Received REFRESH command for switch {}", houseUnitCode);
}
if (x10Function > 0) {
X10Interface x10Interface = cm11aHandler.getX10Interface();
x10Interface.scheduleHWUpdate(this);
} else {
logger.debug("Received invalid command for switch {} command: {}", houseUnitCode, command);
}
} else {
logger.debug("Attempted to change switch to {} for {} because the cm11a is not online", command,
houseUnitCode);
}
}
@Override
public void updateHardware(X10Interface x10Interface) throws IOException, InvalidAddressException {
if (!desiredState.equals(currentState)) {
if (x10Interface.sendFunction(houseUnitCode, x10Function)) {
// Hardware update was successful so update openHAB
updateState(channelUID, desiredState);
setCurrentState(desiredState);
}
}
}
}

View File

@@ -0,0 +1,259 @@
/**
* 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.cm11a.internal.handler;
import java.util.List;
import org.openhab.binding.cm11a.internal.X10Interface;
import org.openhab.binding.cm11a.internal.X10ReceivedData;
import org.openhab.binding.cm11a.internal.X10ReceivedData.X10COMMAND;
import org.openhab.binding.cm11a.internal.config.Cm11aConfig;
import org.openhab.core.library.types.OnOffType;
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.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import gnu.io.NoSuchPortException;
/**
* The {@link Cm11aBridgeHandler} is the Bridge (see
* https://openhab.org/documentation/development/bindings/bridge-handler.html
* for a description of OpenHAB bridges. This gets called first and is responsible for setting up the handler. Mostly it
* loads the 10Interface class which does all of the heavy lifting.
*
* @author Bob Raker - Initial contribution
*/
public class Cm11aBridgeHandler extends BaseBridgeHandler implements ReceivedDataListener {
private Cm11aConfig cm11aConfig;
private X10Interface x10Interface;
private Bridge bridge;
private final Logger logger = LoggerFactory.getLogger(Cm11aBridgeHandler.class);
public Cm11aBridgeHandler(Bridge bridge) {
super(bridge);
this.bridge = bridge;
}
public Cm11aConfig getCm11AConfig() {
return this.cm11aConfig;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Commands are handled by the "Things" which include Cm11aSwitchHandler and Cm11aLampHandler
}
@Override
public void initialize() {
// Get serial port number from config
cm11aConfig = getThing().getConfiguration().as(Cm11aConfig.class);
logger.trace("********* cm11a initialize started *********");
// Verify the configuration is valid
if (!validateConfig(this.cm11aConfig)) {
return;
}
// Initialize the X10 interface
try {
x10Interface = new X10Interface(cm11aConfig.serialPort, this);
x10Interface.setDaemon(true);
x10Interface.start();
x10Interface.addReceivedDataListener(this);
logger.info("Initialized CM11A X10 interface on: {}", cm11aConfig.serialPort);
} catch (NoSuchPortException e) {
x10Interface = null;
logger.error("No such port exists on this machine: {}", cm11aConfig.serialPort);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"No such port exists on this machine: " + cm11aConfig.serialPort);
return;
}
updateStatus(ThingStatus.ONLINE);
}
@Override
public void dispose() {
// Close the serial port
if (x10Interface != null) {
x10Interface.disconnect();
x10Interface = null;
}
logger.debug("Cm11aBridgeHandler is being removed.");
}
/**
* Validate that the configuration is valid
*
* @param cm11aConfig2
* @return
*/
private boolean validateConfig(Cm11aConfig config) {
if (config == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "cm11a configuration missing");
return false;
}
String port = config.serialPort;
if (port == null || port.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "cm11a serialPort not specified");
return false;
}
return true;
}
/*
* (non-Javadoc)
*
* @see org.openhab.binding.cm11a.handler.ReceivedDataListener#receivedX10Data(org.openhab.binding.cm11a.internal.
* X10ReceivedData)
*/
@Override
public void receivedX10Data(X10ReceivedData rd) {
logger.debug("Cm11aReceivedDataManager received the following data: {}", rd);
List<Thing> things = bridge.getThings();
// This block goes through the Things attached to this bridge and find the HouseUnitCode that matches what came
// from the serial port. Then it looks at the channels in that thing and looks for a channel that ends with
// "switchstatus" or "lightlevel"
// which is the one that should be updated.
synchronized (rd) {
for (Thing thing : things) {
String houseUnitCode = (String) thing.getConfiguration().get("houseUnitCode");
for (String messageHouseUnitCode : rd.getAddr()) {
if (messageHouseUnitCode.equals(houseUnitCode)) {
// The channel we want should end in "switchstatus" or "lightlevel". In reality there is
// probably only one channel since these things only define one channel
ChannelUID desiredChannelUid = findX10Channel(thing.getChannels());
if (desiredChannelUid == null) {
return;
}
X10COMMAND cmd = rd.getCmd();
int dims = rd.getDims();
updateX10State(thing, desiredChannelUid, cmd, dims);
}
}
}
}
}
/**
* Find the X10 channel.
*
* @param channels
* @return
*/
private ChannelUID findX10Channel(List<Channel> channels) {
ChannelUID desiredChannelUid = null;
for (Channel channel : channels) {
if (channel.getUID().toString().endsWith("switchstatus")
|| channel.getUID().toString().endsWith("lightlevel")) {
desiredChannelUid = channel.getUID();
break;
}
}
return desiredChannelUid;
}
/**
* Update the X10 state
*
* @param thing
* @param channelUid
* @param cmd
* @param dims
*/
private void updateX10State(Thing thing, ChannelUID channelUid, X10COMMAND cmd, int dims) {
if (thing == null) {
logger.debug("Unable to update X10 state: thing is null");
return;
}
Cm11aAbstractHandler handler = (Cm11aAbstractHandler) thing.getHandler();
if (handler == null) {
logger.debug("Unable to update X10 state: handler is null");
return;
}
// Perform appropriate update based on X10Command received
// Handle ON/OFF commands
if (cmd == X10ReceivedData.X10COMMAND.ON) {
handleUpdate(channelUid, OnOffType.ON);
handler.setCurrentState(OnOffType.ON);
} else if (cmd == X10ReceivedData.X10COMMAND.OFF) {
handleUpdate(channelUid, OnOffType.OFF);
handler.setCurrentState(OnOffType.OFF);
// Handle DIM/Bright commands
} else if (cmd == X10ReceivedData.X10COMMAND.DIM) {
State newState = handler.addDimsToCurrentState(dims);
handleUpdate(channelUid, newState);
handler.setCurrentState(newState);
logger.debug("Current state set to: {}", handler.getCurrentState().toFullString());
} else if (cmd == X10ReceivedData.X10COMMAND.BRIGHT) {
State newState = handler.addBrightsToCurrentState(dims);
handleUpdate(channelUid, newState);
handler.setCurrentState(newState);
logger.debug("Current state set to: {}", handler.getCurrentState().toFullString());
} else {
logger.warn("Received unknown command from cm11a: {}", cmd);
}
}
/**
* The default implementation of this method doesn't do anything. We need it to update the ui as done by the
* updateState function.
* And, update state can not be called directly because it is protected
*/
@Override
public void handleUpdate(ChannelUID channelUID, State newState) {
updateState(channelUID, newState);
}
/**
* Get the X10Interface
*
* @return the X10Interface
*/
public X10Interface getX10Interface() {
return x10Interface;
}
public void changeBridgeStatusToUp() {
if (getThing().getStatus().equals(ThingStatus.OFFLINE)) {
updateStatus(ThingStatus.ONLINE);
logger.debug("Changed the Bridge status to online because the serial interface is working again.");
}
}
public void changeBridgeStatusToDown(String message) {
if (getThing().getStatus().equals(ThingStatus.ONLINE)) {
// Bridge was online but the serial interface is now down
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
logger.debug("Changed the Bridge status to offline because {}.", message);
}
}
}

View File

@@ -0,0 +1,152 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.cm11a.internal.handler;
import java.io.IOException;
import org.openhab.binding.cm11a.internal.InvalidAddressException;
import org.openhab.binding.cm11a.internal.X10Interface;
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.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.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler for Lamp modules. These modules support ON, OFF and brightness level states
*
* @author Bob Raker - Initial contribution
*
*/
public class Cm11aLampHandler extends Cm11aAbstractHandler {
private final Logger logger = LoggerFactory.getLogger(Cm11aLampHandler.class);
private State desiredState = UnDefType.UNDEF;
/**
* Constructor for the Thing
*
* @param thing
*/
public Cm11aLampHandler(Thing thing) {
super(thing);
currentState = OnOffType.ON; // Assume it is on. During refresh it will be turned off and the currentState will
// be updated appropriately
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("**** Cm11aLampHandler handleCommand command = {}, channelUID = {}", command.toString(),
channelUID.getAsString());
x10Function = 0;
Bridge bridge = getBridge();
if (bridge == null) {
logger.debug("Unable to handle command. Bridge is null.");
return;
}
this.channelUID = channelUID;
Cm11aBridgeHandler cm11aHandler = (Cm11aBridgeHandler) bridge.getHandler();
if (cm11aHandler != null && cm11aHandler.getThing().getStatus().equals(ThingStatus.ONLINE)) {
if (OnOffType.ON.equals(command)) {
desiredState = OnOffType.ON;
} else if (OnOffType.OFF.equals(command)) {
desiredState = OnOffType.OFF;
} else if (command instanceof PercentType) {
desiredState = (PercentType) command;
} else if (command instanceof RefreshType) {
// Refresh is triggered by framework during startup.
// Force the lamp off by indicating it is currently on and we want it off
desiredState = PercentType.ZERO; // Start with it off
logger.info("Received REFRESH command for switch {}", houseUnitCode);
} else {
logger.error("Ignoring unknown command received for device: {}", houseUnitCode);
}
if (!(desiredState instanceof UnDefType)) {
X10Interface x10Interface = cm11aHandler.getX10Interface();
x10Interface.scheduleHWUpdate(this);
}
} else {
logger.error("Attempted to change switch {} cm11a is not online", houseUnitCode);
}
}
/*
* (non-Javadoc)
*
* @see org.openhab.binding.cm11a.handler.Cm11aAbstractHandler#updateHardware(org.openhab.binding.cm11a.internal.
* X10Interface)
*/
@Override
public void updateHardware(X10Interface x10Interface) throws IOException, InvalidAddressException {
if (!desiredState.equals(currentState)) {
boolean x10Status = false;
if (desiredState.equals(OnOffType.ON)) {
x10Status = x10Interface.sendFunction(houseUnitCode, X10Interface.FUNC_ON);
} else if (desiredState.equals(OnOffType.OFF)) {
x10Status = x10Interface.sendFunction(houseUnitCode, X10Interface.FUNC_OFF);
} else if (desiredState instanceof PercentType) {
// desiredState must be a PercentType if we got here.
// Calc how many bright increments we need to send (0 to 22)
int desiredPercentFullBright = ((PercentType) desiredState).intValue();
int dims = (desiredPercentFullBright * X10_DIM_INCREMENTS) / 100;
if (currentState.equals(OnOffType.ON)) {
// The current level isn't known because it would have gone to
// the same level as when last turned on. Need to go to full dim and then up to desired
// level.
x10Interface.sendFunction(houseUnitCode, X10Interface.FUNC_DIM, X10_DIM_INCREMENTS);
x10Status = x10Interface.sendFunction(houseUnitCode, X10Interface.FUNC_BRIGHT, dims);
} else if (currentState.equals(OnOffType.OFF)) {
// desiredState must be a PercentType if we got here. And, the light should be off
// We should just be able to send the appropriate number if dims
x10Status = x10Interface.sendFunction(houseUnitCode, X10Interface.FUNC_BRIGHT, dims);
} else if (currentState instanceof PercentType) {
// This is the expected case
// Now currentState and desiredState are both PercentType's
// Need to calc how much to dim or brighten
int currentPercentFullBright = ((PercentType) currentState).intValue();
int percentToBrighten = desiredPercentFullBright - currentPercentFullBright;
int brightens = (percentToBrighten * X10_DIM_INCREMENTS) / 100;
if (brightens > 0) {
x10Status = x10Interface.sendFunction(houseUnitCode, X10Interface.FUNC_BRIGHT, brightens);
} else if (brightens < 0) {
x10Status = x10Interface.sendFunction(houseUnitCode, X10Interface.FUNC_DIM, -brightens);
}
} else {
// Current state is not as expected
logger.warn("Starting state of dimmer was not as expected: {}", currentState.getClass().getName());
}
}
// Now the hardware should have been updated. If successful update the status
if (x10Status) {
// Hardware update was successful so update OpenHAB
updateState(channelUID, desiredState);
setCurrentState(desiredState);
} else {
// Hardware update failed, log
logger.error("cm11a failed to update device: {}", houseUnitCode);
}
}
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.cm11a.internal.handler;
import org.openhab.binding.cm11a.internal.X10ReceivedData;
/**
* Interface to support listening for data received by the cm11a. This needs to be sent to OpenHAB so it has the current
* status of the modules
*
* @author Bob Raker - Initial contribution
*
*/
public interface ReceivedDataListener {
/**
* This is the method called when data is received from the cm11a
*
* @param rd
*/
void receivedX10Data(X10ReceivedData rd);
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="cm11a" 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>CM11A Binding</name>
<description>This is the binding for the X10 Cm11a interface</description>
<author>Bob Raker</author>
</binding:binding>

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="cm11a"
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 which is the cm11a device -->
<bridge-type id="cm11a">
<label>CM11a Controller for X10 Devices</label>
<description>CM11 is a device that allows control of X10 devices for a computer. </description>
<config-description>
<parameter name="serialPort" type="text" required="true">
<label>Serial Port</label>
<context>serial-port</context>
<limitToOptions>false</limitToOptions>
<description>Serial port used to communicate with the CM11a</description>
</parameter>
<parameter name="refresh" type="integer" min="1">
<label>Refresh Interval</label>
<description>Specifies the refresh interval in seconds.</description>
<default>60</default>
</parameter>
</config-description>
</bridge-type>
<!-- For dimable X10 devices attached to the cm11a -->
<thing-type id="dimmer">
<supported-bridge-type-refs>
<bridge-type-ref id="cm11a"/>
</supported-bridge-type-refs>
<label>X10 Dimmer</label>
<description>Controls dimmable loads</description>
<channels>
<channel id="lightlevel" typeId="lightDimmer"/>
</channels>
<config-description>
<parameter name="houseUnitCode" type="text" required="true">
<label>X10 House and Unit Code</label>
<description>Address of dimmer in the X10 system</description>
</parameter>
</config-description>
</thing-type>
<!-- For non-dimable X10 devices attached to the cm11a -->
<thing-type id="switch">
<supported-bridge-type-refs>
<bridge-type-ref id="cm11a"/>
</supported-bridge-type-refs>
<label>X10 Switch</label>
<description>On/off switch</description>
<channels>
<channel id="switchstatus" typeId="switchState"/>
</channels>
<config-description>
<parameter name="houseUnitCode" type="text" required="true">
<label>X10 House and Unit Code</label>
<description>Address of switch in the X10 system</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="lightDimmer">
<item-type>Dimmer</item-type>
<label>Light Level</label>
<description>Increase/decrease the light level</description>
<category>DimmableLight</category>
<state min="0" max="100" pattern="%.0f %%"/>
</channel-type>
<channel-type id="switchState">
<item-type>Switch</item-type>
<label>Switch State</label>
<description>On/off status of the switch</description>
<category>Switch</category>
</channel-type>
</thing:thing-descriptions>