/** * 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); // } // } // // // } }