/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.binding.irtrans.internal;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.irtrans.IRtransBindingProvider;
import org.openhab.binding.tcp.AbstractSocketChannelBinding;
import org.openhab.binding.tcp.Direction;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* Main binding class for the IRtrans binding, based on the AbstractSocketChannelBinding class which also serves
* as a basis for the "regular" TCP binding.
*
* @author Karel Goderis
* @since 1.4.0
*
*/
public class IRtransBinding extends AbstractSocketChannelBinding<IRtransBindingProvider>implements ManagedService {
@SuppressWarnings("restriction")
static private final Logger logger = LoggerFactory.getLogger(IRtransBinding.class);
// Data structure to store the infrared commands that are 'loaded' from the configuration files
// command loading from pre-defined configuration files is not supported (anymore), but the code is maintained
// in case this functionality is re-added in the future
final static protected HashSet<IrCommand> irCommands = new HashSet<IrCommand>();
// time to wait for a reply, in milliseconds
private int timeOut = 1000;
/**
* Structure to store IRTrans infrared commands. See IRTrans documentation for a detailed
* description of the structure of the infrared commands (or wikipedia for infrared in general)
*
*/
private class IrCommand {
/**
*
* Each infrared command is in essence a sequence of characters/pointers that refer
* to pulse/pause timing pairs. So, in order to build an infrared command one has to
* collate the pulse/pause timings as defined by the sequence
*
* PulsePair is a small datastructure to capture each pulse/pair timing pair
*
*/
private class PulsePair {
public int Pulse;
public int Pause;
}
public String remote;
public String command;
public String sequence;
public ArrayList<PulsePair> pulsePairs;
public int numberOfRepeats;
public int frequency;
public int frameLength;
public int pause;
public boolean startBit;
public boolean repeatStartBit;
public boolean noTog;
public boolean rc5;
public boolean rc6;
/**
* Matches two IrCommands
* Commands match if they have the same remote and the same command
*
* @param anotherCommand
* the another command
* @return true, if successful
*/
public boolean matches(IrCommand anotherCommand) {
return (matchRemote(anotherCommand) && matchCommand(anotherCommand));
}
/**
* Match remote fields of two IrCommands
* In everything we do in the IRtrans binding, the "*" stands for a wilcard
* character and will match anything
*
* @param S
* the s
* @return true, if successful
*/
private boolean matchRemote(IrCommand S) {
if (remote.equals("*") || S.remote.equals("*")) {
return true;
} else {
if (S.remote.equals(remote)) {
return true;
} else {
return false;
}
}
}
/**
* Match command fields of two IrCommands
*
* @param S
* the s
* @return true, if successful
*/
private boolean matchCommand(IrCommand S) {
if (command.equals("*") || S.command.equals("*")) {
return true;
} else {
if (S.command.equals(command)) {
return true;
} else {
return false;
}
}
}
/**
* Convert/Parse the IRCommand into a ByteBuffer that is compatible with the IRTrans devices
*
* @return the byte buffer
*/
@SuppressWarnings("restriction")
public ByteBuffer toByteBuffer() {
ByteBuffer byteBuffer = ByteBuffer.allocate(44 + 210 + 1);
// skip first byte for length - we will fill it in at the end
byteBuffer.position(1);
// Checksum - 1 byte - not used in the ethernet version of the device
byteBuffer.put((byte) 0);
// Command - 1 byte - not used
byteBuffer.put((byte) 0);
// Address - 1 byte - not used
byteBuffer.put((byte) 0);
// Mask - 2 bytes - not used
byteBuffer.putShort((short) 0);
// Number of pulse pairs - 1 byte
try {
byte[] byteSequence = sequence.getBytes("ASCII");
byteBuffer.put((byte) (byteSequence.length));
} catch (UnsupportedEncodingException e) {
logger.debug("An exception occurred while encoding a bytebuffer");
}
// Frequency - 1 byte
byteBuffer.put((byte) frequency);
// Mode / Flags - 1 byte
byte modeFlags = 0;
if (startBit) {
modeFlags = (byte) (modeFlags | 1);
}
if (repeatStartBit) {
modeFlags = (byte) (modeFlags | 2);
}
if (rc5) {
modeFlags = (byte) (modeFlags | 4);
}
if (rc6) {
modeFlags = (byte) (modeFlags | 8);
}
byteBuffer.put(modeFlags);
// Pause timings - 8 Shorts = 16 bytes
for (int i = 0; i < pulsePairs.size(); i++) {
byteBuffer.putShort((short) Math.round(pulsePairs.get(i).Pause / 8));
}
for (int i = pulsePairs.size(); i <= 7; i++) {
byteBuffer.putShort((short) 0);
}
// Pulse timings - 8 Shorts = 16 bytes
for (int i = 0; i < pulsePairs.size(); i++) {
byteBuffer.putShort((short) Math.round(pulsePairs.get(i).Pulse / 8));
}
for (int i = pulsePairs.size(); i <= 7; i++) {
byteBuffer.putShort((short) 0);
}
// Time Counts - 1 Byte
byteBuffer.put((byte) pulsePairs.size());
// Repeats - 1 Byte
byte repeat = (byte) 0;
repeat = (byte) numberOfRepeats;
if (frameLength > 0) {
repeat = (byte) (repeat | 128);
}
byteBuffer.put(repeat);
// Repeat Pause or Frame Length - 1 byte
if ((repeat & 128) == 128) {
byteBuffer.put((byte) frameLength);
} else {
byteBuffer.put((byte) pause);
}
// IR pulse sequence
try {
byteBuffer.put(sequence.getBytes("ASCII"));
} catch (UnsupportedEncodingException e) {
}
// Add <CR> (ASCII 13) at the end of the sequence
byteBuffer.put((byte) ((char) 13));
// set the length of the byte sequence
byteBuffer.flip();
byteBuffer.position(0);
byteBuffer.put((byte) (byteBuffer.limit() - 1));
byteBuffer.position(0);
return byteBuffer;
}
/**
* Convert the the infrared command to a Hexadecimal notation/string that can be
* interpreted by the IRTrans device
*
* Convert the first 44 bytes to hex notation, then copy the remainder
* (= IR command piece) as ASCII string
*
* @return the byte buffer in Hex format
*/
public ByteBuffer toHEXByteBuffer() {
byte hexDigit[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
ByteBuffer byteBuffer = toByteBuffer();
byte[] toConvert = new byte[byteBuffer.limit()];
byteBuffer.get(toConvert, 0, byteBuffer.limit());
byte[] converted = new byte[toConvert.length * 2];
for (int k = 0; k < toConvert.length - 1; k++) {
converted[2 * k] = hexDigit[(toConvert[k] >> 4) & 0x0f];
converted[2 * k + 1] = hexDigit[toConvert[k] & 0x0f];
}
ByteBuffer convertedBuffer = ByteBuffer.allocate(converted.length);
convertedBuffer.put(converted);
convertedBuffer.flip();
return convertedBuffer;
}
/**
* Convert 'sequence' bit of the IRTrans compatible byte buffer to a Hexidecimal string
*
* @return the string
*/
public String sequenceToHEXString() {
byte[] byteArray = toHEXByteArray();
return new String(byteArray, 88, byteArray.length - 88 - 2);
}
/**
* Convert the IRTrans compatible byte buffer to a string
*
* @return the string
*/
public String toHEXString() {
return new String(toHEXByteArray());
}
/**
* Convert the IRTrans compatible byte buffer to a byte array.
*
* @return the byte[]
*/
public byte[] toHEXByteArray() {
return toHEXByteBuffer().array();
}
}
protected void addBindingProvider(IRtransBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(IRtransBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings({ "rawtypes", "restriction" })
public void updated(Dictionary config) throws ConfigurationException {
super.updated(config);
if (config != null) {
String timeOutString = (String) config.get("timeout");
if (StringUtils.isNotBlank(timeOutString)) {
timeOut = Integer.parseInt((timeOutString));
} else {
logger.info(
"The maximum time out for blocking write operations will be set to the default vaulue of {}",
timeOut);
}
}
// Prevent users from doing funny stuff with this binding
useAddressMask = false;
itemShareChannels = true;
bindingShareChannels = true;
directionsShareChannels = false;
maximumBufferSize = 1024;
setProperlyConfigured(true);
}
/**
* {@inheritDoc}
*/
@Override
protected boolean internalReceiveChanneledCommand(String itemName, Command command, Channel sChannel,
String commandAsString) {
IRtransBindingProvider provider = findFirstMatchingBindingProvider(itemName);
String remoteName = null;
String irCommandName = null;
if (command != null && provider != null) {
if (command instanceof DecimalType) {
remoteName = StringUtils.substringBefore(commandAsString, ",");
irCommandName = StringUtils.substringAfter(commandAsString, ",");
IrCommand firstCommand = new IrCommand();
firstCommand.remote = remoteName;
firstCommand.command = irCommandName;
IrCommand secondCommand = new IrCommand();
secondCommand.remote = provider.getRemote(itemName, command);
secondCommand.command = provider.getIrCommand(itemName, command);
if (!firstCommand.matches(secondCommand)) {
remoteName = null;
irCommandName = null;
}
} else {
remoteName = provider.getRemote(itemName, command);
irCommandName = provider.getIrCommand(itemName, command);
}
}
if (remoteName != null && irCommandName != null) {
Leds led = provider.getLed(itemName, command);
IrCommand theCommand = new IrCommand();
theCommand.remote = remoteName;
theCommand.command = irCommandName;
// construct the string we need to send to the IRtrans device
String output = packIRDBCommand(led, theCommand);
ByteBuffer outputBuffer = ByteBuffer.allocate(output.getBytes().length);
ByteBuffer response = null;
try {
outputBuffer.put(output.getBytes("ASCII"));
response = writeBuffer(outputBuffer, sChannel, true, timeOut);
} catch (UnsupportedEncodingException e) {
logger.error("An exception occurred while encoding an infrared command: {}", e.getMessage());
}
if (response != null) {
String message = stripByteCount(response);
if (message != null) {
if (message.contains("RESULT OK")) {
List<Class<? extends State>> stateTypeList = provider.getAcceptedDataTypes(itemName, command);
State newState = createStateFromString(stateTypeList, commandAsString);
if (newState != null) {
eventPublisher.postUpdate(itemName, newState);
} else {
logger.warn("Can not parse " + commandAsString + " to match command {} on item {} ",
command, itemName);
}
} else {
logger.warn("Received an unexpected response {}",
StringUtils.substringAfter(message, "RESULT "));
}
}
} else {
logger.warn("Did not receive an answer from the IRtrans device - Parsing is skipped");
}
} else {
logger.warn("Invalid command {} for Item {} - Transmission is skipped", commandAsString, itemName);
}
// we will always return false, because the irBinding itself will postUpdate new values, the underlying tcp
// binding should
// not deal with that anymore.
return false;
}
/**
* Main method to parse ASCII string received from the IR Transceiver.
*
* @param qualifiedItems
* the qualified items, e.g. only those items that have the host:port combination
* in its binding configuration string will "receive" the ASCII string from the
* IRTrans device with host:port
* @param byteBuffer
* the byte buffer
*/
@Override
protected void parseBuffer(String itemName, Command aCommand, Direction theDirection, ByteBuffer byteBuffer) {
String message = stripByteCount(byteBuffer);
if (message != null) {
// IRTrans devices return "RESULT OK" when it succeeds to emit an infrared sequence
if (message.contains("RESULT OK")) {
parseOKMessage(itemName, message);
}
// IRTrans devices return a string starting with RCV_HEX each time it captures an
// infrared sequence from a remote control
if (message.contains("RCV_HEX")) {
parseHexMessage(itemName, message, aCommand);
}
// IRTrans devices return a string starting with RCV_COM each time it captures an
// infrared sequence from a remote control that is stored in the device's internal dB
if (message.contains("RCV_COM")) {
parseIRDBMessage(itemName, message, aCommand);
}
} else {
logger.warn("Received some non-compliant garbage ({})- Parsing is skipped", byteBuffer.toString());
}
}
/**
* Parses the rcv hex.
*
* @param itemName
* the qualified items
* @param message
* the message
* @param ohCommand
* the openHAB command
*/
protected void parseHexMessage(String itemName, String message, Command ohCommand) {
Pattern HEX_PATTERN = Pattern.compile("RCV_HEX (.*)");
Matcher matcher = HEX_PATTERN.matcher(message);
if (matcher.matches()) {
String command = matcher.group(1);
IrCommand theCommand = getIrCommand(command);
if (theCommand != null) {
parseDecodedCommand(itemName, theCommand, ohCommand);
} else {
logger.error("{} does not match any know IRtrans command", command);
}
} else {
logger.error("{} does not match the IRtrans message format ({})", message, matcher.pattern());
}
}
protected void parseIRDBMessage(String itemName, String message, Command ohCommand) {
Pattern IRDB_PATTERN = Pattern.compile("RCV_COM (.*),(.*),(.*),(.*)");
Matcher matcher = IRDB_PATTERN.matcher(message);
if (matcher.matches()) {
IrCommand theCommand = new IrCommand();
theCommand.remote = matcher.group(1);
theCommand.command = matcher.group(2);
parseDecodedCommand(itemName, theCommand, ohCommand);
} else {
logger.error("{} does not match the IRDB IRtrans message format ({})", message, matcher.pattern());
}
}
protected void parseDecodedCommand(String itemName, IrCommand theCommand, Command ohCommand) {
if (theCommand != null) {
// traverse the providers, for each provider, check each binding if it matches theCommand
for (IRtransBindingProvider provider : providers) {
if (provider.providesBindingFor(itemName)) {
List<org.openhab.core.types.Command> commands = provider.getAllCommands(itemName);
// first check if commands are defined, and that they have the correct DirectionType
Iterator<org.openhab.core.types.Command> listIterator = commands.listIterator();
while (listIterator.hasNext()) {
org.openhab.core.types.Command aCommand = listIterator.next();
IrCommand providerCommand = new IrCommand();
providerCommand.remote = provider.getRemote(itemName, aCommand);
providerCommand.command = provider.getIrCommand(itemName, aCommand);
if (aCommand == ohCommand) {
if (providerCommand.matches(theCommand)) {
List<Class<? extends State>> stateTypeList = provider.getAcceptedDataTypes(itemName,
aCommand);
State newState = null;
if (aCommand instanceof DecimalType) {
newState = createStateFromString(stateTypeList,
theCommand.remote + "," + theCommand.command);
} else {
newState = createStateFromString(stateTypeList, aCommand.toString());
}
if (newState != null) {
eventPublisher.postUpdate(itemName, newState);
} else {
logger.warn("Can not create an Item State to match command {} on item {} ",
aCommand, itemName);
}
} else {
logger.info(
"The IRtrans command '{},{}' does not match the command '{}' of the binding configuration for item '{}'",
new Object[] { theCommand.remote, theCommand.command, ohCommand, itemName });
}
}
}
}
}
}
}
/**
* Parses the result send.
*
* @param itemName
* the qualified items
* @param message
* the message
*/
protected void parseOKMessage(String itemName, String message) {
// nothing interesting to do here
}
/**
* "Pack" the infrared command so that it can be sent to the IRTrans device
*
* @param led
* the led
* @param theCommand
* the the command
* @return a string which is the full command to be sent to the device
*/
protected String packHexCommand(Leds led, IrCommand theCommand) {
String output = new String();
output = "Asndhex ";
output += "L";
switch (led) {
case ALL:
output += "B";
break;
case ONE:
output += "1";
break;
case TWO:
output += "2";
break;
case THREE:
output += "3";
break;
case FOUR:
output += "4";
break;
case FIVE:
output += "5";
break;
case SIX:
output += "6";
break;
case SEVEN:
output += "7";
break;
case EIGHT:
output += "8";
break;
case INTERNAL:
output += "I";
break;
case EXTERNAL:
output += "E";
break;
case DEFAULT:
output += "D";
break;
}
output += ",";
output += "H" + theCommand.toHEXString();
output += (char) 13;
return output;
}
/**
* "Pack" the infrared command so that it can be sent to the IRTrans device
*
* @param led
* the led
* @param theCommand
* the the command
* @return a string which is the full command to be sent to the device
*/
protected String packIRDBCommand(Leds led, IrCommand theCommand) {
String output = new String();
output = "Asnd ";
output += theCommand.remote;
output += ",";
output += theCommand.command;
output += ",l";
switch (led) {
case ALL:
output += "B";
break;
case ONE:
output += "1";
break;
case TWO:
output += "2";
break;
case THREE:
output += "3";
break;
case FOUR:
output += "4";
break;
case FIVE:
output += "5";
break;
case SIX:
output += "6";
break;
case SEVEN:
output += "7";
break;
case EIGHT:
output += "8";
break;
case INTERNAL:
output += "I";
break;
case EXTERNAL:
output += "E";
break;
case DEFAULT:
output += "D";
break;
}
output += "\r\n";
return output;
}
/**
* Strip byte count from the bytebuffer. IRTrans devices include the number of bytes sent
* in each response it sends back to the connected host. This is a simple error checking
* mechanism - we do need that information, and so, we strip it
*
* @param byteBuffer
* the byte buffer
* @return the string
*/
protected String stripByteCount(ByteBuffer byteBuffer) {
/** {@link Pattern} which matches a binding configuration part */
Pattern RESPONSE_PATTERN = Pattern.compile("..(\\d{5}) (.*)");
String message = null;
String response = new String(byteBuffer.array(), 0, byteBuffer.limit());
response = StringUtils.chomp(response);
Matcher matcher = RESPONSE_PATTERN.matcher(response);
if (matcher.matches()) {
String byteCountAsString = matcher.group(1);
int byteCount = Integer.parseInt(byteCountAsString);
message = matcher.group(2);
}
return message;
}
/**
* Fetch the IrCommand that corresponds with the given (hex)String.
*
* @param someString
* the some string
* @return the ir command
*/
protected IrCommand getIrCommand(String someString) {
IrCommand theCommand = null;
if (someString != null) {
// Run through the dB if IrCommands to see which one is matching, if any, the payload we just received
Iterator<IrCommand> commandIterator = irCommands.iterator();
while (commandIterator.hasNext()) {
IrCommand aCommand = commandIterator.next();
if (aCommand.sequenceToHEXString().equals(someString)) {
theCommand = aCommand;
break;
}
}
}
return theCommand;
}
/**
* Fetch the IrCommand for a given remote:command combination
*
* @param remote
* the remote
* @param command
* the command
* @return the ir command
*/
protected IrCommand getIrCommand(String remote, String command) {
IrCommand theCommand = null;
if (remote != null && command != null) {
// Run through the dB if IrCommands to see which one is matching, if any, the payload we just received
Iterator<IrCommand> commandIterator = irCommands.iterator();
while (commandIterator.hasNext()) {
IrCommand aCommand = commandIterator.next();
if (aCommand.remote.equals(remote) && aCommand.command.equals(command)) {
theCommand = aCommand;
break;
}
}
}
return theCommand;
}
/**
* {@inheritDoc}
*/
@Override
protected void configureChannel(Channel channel) {
String putInASCIImode = "ASCI";
ByteBuffer byteBuffer = ByteBuffer.allocate(4);
try {
byteBuffer.put(putInASCIImode.getBytes("ASCII"));
writeBuffer(byteBuffer, channel, false, timeOut);
} catch (UnsupportedEncodingException e) {
logger.error("An exception occurred while configuring the IRtrans device: {}", e.getMessage());
}
String getFirmwareVersion = "Aver" + (char) 13;
ByteBuffer response = null;
byteBuffer = ByteBuffer.allocate(5);
try {
byteBuffer.put(getFirmwareVersion.getBytes("ASCII"));
response = writeBuffer(byteBuffer, channel, true, timeOut);
} catch (UnsupportedEncodingException e) {
logger.error("An exception occurred while configuring the IRtrans device: {}", e.getMessage());
}
if (response != null) {
String message = stripByteCount(response);
if (message != null) {
if (message.contains("VERSION")) {
logger.info("Found an IRtrans device with firmware {}", message);
} else {
logger.warn("Received some non-compliant garbage ({})", message);
}
}
} else {
logger.warn("Did not receive an answer from the IRtrans device - Parsing is skipped");
}
}
/**
* @{inheritDoc}
*/
@Override
protected String getName() {
return "IRtrans Refresh Service";
}
// FOR FUTURE USAGE - ORG.OPENHAB.MODEL.* RELATED - Parsing of IRtrans .rem files
// KEPT IN PLACE IN CASE A NEW MODEL RELATED PARSING APPROACH IS CHOSEN IN THE FUTURE
// /**
// * Populates our infrared command database with the contents of the IRTrans .rem config files
// *
// * @param aModel
// *
// */
// private void addIRtransModel(IRtransModel aModel){
// String remoteName = aModel.getRemote().getName();
//
// for (org.openhab.model.irtrans.IRtrans.Command aCommand : aModel.getCommands()) {
//
// int timingId = Integer.parseInt(aCommand.getTiming());
//
// Timing theTiming = null;
// for (Timing atiming: aModel.getTimings()) {
// if(Integer.parseInt(atiming.getId()) == timingId)
// {
// theTiming = atiming;
// break;
// }
// }
//
// if(theTiming != null && Integer.parseInt(theTiming.getCount()) == theTiming.getPairs().size()) {
// IrCommand newCommand = new IrCommand();
// newCommand.remote = remoteName;
// newCommand.command = aCommand.getName();
// newCommand.sequence = aCommand.getCommand();
//
// newCommand.pulsePairs = new ArrayList<PulsePair>();
//
// for(int i=1;i<=Integer.parseInt(theTiming.getCount());i++ ) {
// PulsePair newPair = new PulsePair();
//
// TimingPair thePair = null;
//
// for (TimingPair aPair: theTiming.getPairs()) {
// if(Integer.parseInt(aPair.getId()) == i){
// thePair = aPair;
// break;
// }
// }
//
// newPair.Pause = Integer.parseInt(thePair.getPause());
// newPair.Pulse = Integer.parseInt(thePair.getPulse());
//
// newCommand.pulsePairs.add(newPair);
// }
//
// TimingOptions theOptions = theTiming.getOptions();
// if(theOptions.getFrequency()!=null)newCommand.frequency = Integer.parseInt(theOptions.getFrequency());
// if(theOptions.getLength()!=null)newCommand.frameLength = Integer.parseInt(theOptions.getLength());
// if(theOptions.getPause()!=null) newCommand.pause = Integer.parseInt(theOptions.getPause());
// if(theOptions.getRepeats()!=null)newCommand.numberOfRepeats = Integer.parseInt(theOptions.getRepeats());
// newCommand.startBit = theOptions.isStartbit();
// newCommand.noTog = theOptions.isNotog();
// newCommand.rc5 = theOptions.isRc5();
// newCommand.rc6 = theOptions.isRc6();
// newCommand.repeatStartBit = theOptions.isRepeatstartbit();
//
// irCommands.add(newCommand);
//
// }
// }
// }
//
// /**
// * Removes the ir trans model.
// *
// * @param aModel
// * the a model
// */
// private void removeIRtransModel(IRtransModel aModel){
// String remoteName = aModel.getRemote().getName();
//
// Iterator<IrCommand> commandIterator = irCommands.iterator();
// while(commandIterator.hasNext()){
// IrCommand aCommand = commandIterator.next();
// if(aCommand.remote.equals(remoteName)) {
// irCommands.remove(aCommand);
// }
// }
//
//
// }
}