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