From 3cb79d31299f0f0f60f01716d3bbbd3428b895f4 Mon Sep 17 00:00:00 2001 From: Karel Goderis Date: Fri, 3 Oct 2014 15:25:13 +0200 Subject: [PATCH] Initial contribution of IRtrans 2.0 binding Signed-off-by: Karel Goderis (github: kgoderis) --- .../org.openhab.binding.irtrans/.classpath | 7 + .../org.openhab.binding.irtrans/.project | 33 + .../ESH-INF/binding/binding.xml | 9 + .../ESH-INF/thing/blaster.xml | 43 + .../ESH-INF/thing/ethernetbridge.xml | 84 ++ .../META-INF/MANIFEST.MF | 19 + .../OSGI-INF/IRtransHandlerFactory.xml | 19 + .../org.openhab.binding.irtrans/README.md | 97 ++ .../org.openhab.binding.irtrans/about.html | 29 + .../build.properties | 6 + .../org.openhab.binding.irtrans/pom.xml | 24 + .../openhab/binding/irtrans/IRcommand.java | 277 +++++ .../irtrans/IRtransBindingConstants.java | 74 ++ .../factory/IRtransHandlerFactory.java | 88 ++ .../irtrans/handler/BlasterHandler.java | 122 ++ .../handler/EthernetBridgeHandler.java | 1026 +++++++++++++++++ .../handler/TransceiverStatusListener.java | 48 + 17 files changed, 2005 insertions(+) create mode 100644 addons/binding/org.openhab.binding.irtrans/.classpath create mode 100644 addons/binding/org.openhab.binding.irtrans/.project create mode 100644 addons/binding/org.openhab.binding.irtrans/ESH-INF/binding/binding.xml create mode 100644 addons/binding/org.openhab.binding.irtrans/ESH-INF/thing/blaster.xml create mode 100644 addons/binding/org.openhab.binding.irtrans/ESH-INF/thing/ethernetbridge.xml create mode 100644 addons/binding/org.openhab.binding.irtrans/META-INF/MANIFEST.MF create mode 100644 addons/binding/org.openhab.binding.irtrans/OSGI-INF/IRtransHandlerFactory.xml create mode 100644 addons/binding/org.openhab.binding.irtrans/README.md create mode 100644 addons/binding/org.openhab.binding.irtrans/about.html create mode 100644 addons/binding/org.openhab.binding.irtrans/build.properties create mode 100644 addons/binding/org.openhab.binding.irtrans/pom.xml create mode 100644 addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/IRcommand.java create mode 100644 addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/IRtransBindingConstants.java create mode 100644 addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/factory/IRtransHandlerFactory.java create mode 100644 addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/handler/BlasterHandler.java create mode 100644 addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/handler/EthernetBridgeHandler.java create mode 100644 addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/handler/TransceiverStatusListener.java diff --git a/addons/binding/org.openhab.binding.irtrans/.classpath b/addons/binding/org.openhab.binding.irtrans/.classpath new file mode 100644 index 0000000000000..a95e0906ca013 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/addons/binding/org.openhab.binding.irtrans/.project b/addons/binding/org.openhab.binding.irtrans/.project new file mode 100644 index 0000000000000..8920dab6e82a4 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/.project @@ -0,0 +1,33 @@ + + + org.openhab.binding.irtrans + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/addons/binding/org.openhab.binding.irtrans/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.irtrans/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..d96e7134373e8 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/ESH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + IRtrans Binding + This is the binding for IRtrans (www.irtrans.de) Transceivers + Karel Goderis + + diff --git a/addons/binding/org.openhab.binding.irtrans/ESH-INF/thing/blaster.xml b/addons/binding/org.openhab.binding.irtrans/ESH-INF/thing/blaster.xml new file mode 100644 index 0000000000000..d79db29447665 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/ESH-INF/thing/blaster.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + This is an infrared transmitter that can send infrared commands + + + + + + + + + The Led on which infrared commands will be emitted + + + + + The remote or manufacturer name which's commands will be allowed, as defined in the IRtrans server database and flashed into the transceiver. Can be '*' for any remote + + + + + The name of the command will be allowed, as defined in the IRtrans server database and flashed into the transceiver. Can be '*' for any command + + + + + + String + + The IO channel allows to read infrared commands received by the blaster, as well as to write infrared commands to be sent by the blaster + + + diff --git a/addons/binding/org.openhab.binding.irtrans/ESH-INF/thing/ethernetbridge.xml b/addons/binding/org.openhab.binding.irtrans/ESH-INF/thing/ethernetbridge.xml new file mode 100644 index 0000000000000..62018477cde86 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/ESH-INF/thing/ethernetbridge.xml @@ -0,0 +1,84 @@ + + + + + + + This Thing supports the Ethernet (PoE) IRtrans transceiver equipped with an on-board IRDB database + + + + + Network address of the ethernet transceiver + + + + TCP port number of the transceiver service + + + + Buffer size used by the TCP socket when sending and receiving commands to the transceiver + 1024 + + + + Specifies the refresh interval in milliseconds. + 50 + + + + Specifies the time milliseconds to wait for a response from the transceiver when sending a command. + 100 + + + + Specifies the time milliseconds to wait for a response from the transceiver when pinging the device + 1000 + + + + Specifies the time seconds to wait before reconnecting to a transceiver after a communication failure + 10 + + + + + + String + + The Blaster Channel allows to send (filtered) infrared commands over the specified blaster led of the transceiver + + + + The Led on which infrared commands will be emitted + + + + The remote or manufacturer name which's commands will be allowed, as defined in the IRtrans server database and flashed into the transceiver. Can be '*' for any remote + + + + The name of the command will be allowed, as defined in the IRtrans server database and flashed into the transceiver. Can be '*' for any command + + + + + + String + + The Receiver Channel allows to receive (filtered) infrared commands on the receiver led of the transceiver + + + + The remote or manufacturer name which's commands will be allowed, as defined in the IRtrans server database and flashed into the transceiver. Can be '*' for any remote + + + + The name of the command will be allowed, as defined in the IRtrans server database and flashed into the transceiver. Can be '*' for any command + + + + + diff --git a/addons/binding/org.openhab.binding.irtrans/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.irtrans/META-INF/MANIFEST.MF new file mode 100644 index 0000000000000..9e25d16747625 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/META-INF/MANIFEST.MF @@ -0,0 +1,19 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: IRtrans Binding +Bundle-SymbolicName: org.openhab.binding.irtrans;singleton:=true +Bundle-Vendor: openHAB +Bundle-Version: 2.1.0.qualifier +Bundle-RequiredExecutionEnvironment: JavaSE-1.7 +Bundle-ClassPath: . +Import-Package: com.google.common.collect, + org.eclipse.smarthome.config.core, + org.eclipse.smarthome.core.library.types, + org.eclipse.smarthome.core.thing, + org.eclipse.smarthome.core.thing.binding, + org.eclipse.smarthome.core.types, + org.slf4j, + org.apache.commons.lang +Service-Component: OSGI-INF/*.xml +Export-Package: org.openhab.binding.irtrans, + org.openhab.binding.irtrans.handler diff --git a/addons/binding/org.openhab.binding.irtrans/OSGI-INF/IRtransHandlerFactory.xml b/addons/binding/org.openhab.binding.irtrans/OSGI-INF/IRtransHandlerFactory.xml new file mode 100644 index 0000000000000..db68f8f8fc5f5 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/OSGI-INF/IRtransHandlerFactory.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/addons/binding/org.openhab.binding.irtrans/README.md b/addons/binding/org.openhab.binding.irtrans/README.md new file mode 100644 index 0000000000000..12e8f4977d2f2 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/README.md @@ -0,0 +1,97 @@ +# IRtrans Binding + +This binding integrates infrared receivers and blasters manufactured by IRtrans (www.irtrans.de) + +## Supported Things + +The *ethernet* Bridge supports the Ethernet (PoE) IRtrans transceiver equipped with an on-board IRDB database. Blasters and receivers are defined as Channels on the Bridge, but one can also define blasters as a *blaster* child Thing on the Bridge. + +## Discovery + +There is no Discovery feature available. + +## Binding Configuration + +There is no specific binding configuration required. + +## Thing Configuration + +The *ethernet* Bridge requires an *ipAddress* IP address and *portNumber* TCP port number in order to configure it. Optionally, one can add the following parameters to the configuration: + +*bufferSize* : Buffer size used by the TCP socket when sending and receiving commands to the transceiver (default: 1024) +*refreshInterval* : Specifies the refresh interval, in milliseconds, for status updates (default: 50) +*responseTimeOut* : Specifies the time milliseconds to wait for a response from the transceiver when sending a command (default: 100) +*pingTimeOut* : Specifies the time milliseconds to wait for a response from the transceiver when pinging the device (default: 1000) +*reconnectInterval* : Specifies the time seconds to wait before reconnecting to a transceiver after a communication failure (default: 10) + +The *blaster* Thing requires a *led* parameter to specify on which infrared commands will be emitted, *remote* the remote or manufacturer name which's commands will be allowed, as defined in the IRtrans server database that is flashed into the transceiver (can be '*' for 'any' remote), and *command* the name of the command will be allowed, as defined in the IRtrans server database that is flashed into the transceiver (can be '*' for 'any' command). + +## Channels + +The *ethernet* Thing supports the following Channel Types: + +| Channel Type ID | Item Type | Description | +|-----------------|------------------------|--------------|----------------- |------------- | +| blaster | String | Send (filtered) infrared commands over the specified blaster led of the transceiver | +| receiver | String | Receive (filtered) infrared commands on the receiver led of the transceiver | + +The *blaster* Channel Type requires a *led* configuration parameter to specify on which infrared commands will be emitted, *remote* the remote or manufacturer name which's commands will be allowed, as defined in the IRtrans server database that is flashed into the transceiver (can be '*' for 'any' remote), and *command* the name of the command will be allowed, as defined in the IRtrans server database that is flashed into the transceiver (can be '*' for 'any' command). + +The *receiver* Channel Type requires *remote* the remote or manufacturer name which's commands will be allowed, as defined in the IRtrans server database that is flashed into the transceiver (can be '*' for 'any' remote), and *command* the name of the command will be allowed, as defined in the IRtrans server database that is flashed into the transceiver (can be '*' for 'any' command). + +The *blaster* Thing supports a *io* Channel (of Item Type String) that allows to read infrared commands received by the blaster, as well as to write infrared commands to be sent by the blaster. + +The IRtrans transceivers store infrared commands in a "remote,command" table, e.g. "telenet,power". Sending the literal text string "telenet,power" to the transceiver will make the transceiver "translate" that into the actual infrared command that will be emitted by the transceiver. A "remote,command" string sent to a Channel that does not match the defined filter will be ignored. + +## Full Example + +demo.things: +``` +Bridge irtrans:ethernet:kitchen [ ipAddress="192.168.0.56", portNumber=21000, bufferSize=1024, refreshInterval=50, responseTimeOut=100, pingTimeOut=2000, reconnectInterval=10 ] +{ +Channels: +Receiver : any [remote="*", command="*"] +Receiver : telenet_power [remote="telenet", command="power"] +Blaster : samsung [led="E", remote="samsung", command="*"] +} +``` + +In the above example, the first channel will be updated when any IR command from any type of device is received. The second channel will only be updated if a "power" infrared command from the remote/device type "telenet" is received. The third channel can be used to feed any type of infrared command to a Samsung television by means of the "E" emitter of the IRtrans device. + +``` +Bridge irtrans:ethernet:technicalfacilities [ ipAddress="192.168.0.58", portNumber=21000, bufferSize=1024, refreshInterval=50, responseTimeOut=100, pingTimeOut=2000, reconnectInterval=10 ] +{ +Channels: +Receiver : any [remote="*", command="*"] +Blaster : telenet1 [led="2", remote="telenet", command="*"] +Blaster : telenet2 [led="1", remote="telenet", command="*"] +Blaster : appletv [led="3", remote="appletv", command="*"] +} +``` + +In the above channel a single IRtrans transceiver has 3 output leds in use, 2 to drive 2 DTV SetTopBoxes, and a third one to drive an Apple TV device. + +demo.items: +``` +String KitchenIRReceiverAny {channel="irtrans:ethernet:kitchen:any"} +String KitchenIRReceiverTelenetPower {channel="irtrans:ethernet:kitchen:telenet_power"} +String KitchenIRBlasterSamsung {channel="irtrans:ethernet:kitchen:samsung"} + +String TechnicalFacilitiesIRReceiverAny {channel="irtrans:ethernet:technicalfacilities:any"} +String TechnicalFacilitiesIRBlasterTelenet2 {channel="irtrans:ethernet:technicalfacilities:telenet2"} +String TechnicalFacilitiesIRBlasterTelenet1 {channel="irtrans:ethernet:technicalfacilities:telenet1"} +String TechnicalFacilitiesIRBlasterAppleTV {channel="irtrans:ethernet:technicalfacilities:appletv"} +``` + +demo.rules: + +``` +rule "Kitchen switch IR rule" +when + Item KitchenIRReceiverTelenetPower received update +then + createTimer(now.plusSeconds(5)) [| + KitchenIRBlasterSamsung.sendCommand("samsung,power") + ] +end +``` \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.irtrans/about.html b/addons/binding/org.openhab.binding.irtrans/about.html new file mode 100644 index 0000000000000..3d2a6c8491e7b --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/about.html @@ -0,0 +1,29 @@ + + + + +About + + +

About This Content

+ +

April 11, 2015

+

License

+ +

The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise +indicated below, the Content is provided to you under the terms and conditions of the +Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is available +at http://www.eclipse.org/legal/epl-v10.html. +For purposes of the EPL, "Program" will mean the Content.

+ +

If you did not receive this Content directly from the Eclipse Foundation, the Content is +being redistributed by another party ("Redistributor") and different terms and conditions may +apply to your use of any object code in the Content. Check the Redistributor's license that was +provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise +indicated below, the terms and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at http://www.eclipse.org.

+ + + + \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.irtrans/build.properties b/addons/binding/org.openhab.binding.irtrans/build.properties new file mode 100644 index 0000000000000..66e21b90751a7 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/build.properties @@ -0,0 +1,6 @@ +source.. = src/main/java/ +output.. = target/classes +bin.includes = META-INF/,\ + .,\ + OSGI-INF/,\ + ESH-INF/ \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.irtrans/pom.xml b/addons/binding/org.openhab.binding.irtrans/pom.xml new file mode 100644 index 0000000000000..3773364827f86 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/pom.xml @@ -0,0 +1,24 @@ + + + + 4.0.0 + + + org.openhab.binding + pom + 2.1.0-SNAPSHOT + + + + org.openhab.binding.irtrans + org.openhab.binding.irtrans + + + org.openhab.binding + org.openhab.binding.irtrans + 2.1.0-SNAPSHOT + + IRtrans Binding + eclipse-plugin + + diff --git a/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/IRcommand.java b/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/IRcommand.java new file mode 100644 index 0000000000000..2dc04322251bd --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/IRcommand.java @@ -0,0 +1,277 @@ +/** + * Copyright (c) 2014 openHAB UG (haftungsbeschraenkt) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.irtrans; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link IRcommand} is a structure to store and manipulate infrared command + * in various formats + * + * @author Karel Goderis - Initial contribution + * @since 2.0.0 + * + */ +public class IRcommand { + + private Logger logger = LoggerFactory.getLogger(IRcommand.class); + + /** + * + * 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 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 + */ + 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 (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(); + } + +} diff --git a/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/IRtransBindingConstants.java b/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/IRtransBindingConstants.java new file mode 100644 index 0000000000000..6585c60e0a8a4 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/IRtransBindingConstants.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2014 openHAB UG (haftungsbeschraenkt) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.irtrans; + +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link IRtransBindingConstants} contains constants used by the IRtrans + * handler classes + * + * @author Karel Goderis - Initial contribution + * @since 2.0.0 + * + **/ +public class IRtransBindingConstants { + + public static final String BINDING_ID = "irtrans"; + + // List of all Thing Type UIDs + public final static ThingTypeUID THING_TYPE_ETHERNET_BRIDGE = new ThingTypeUID(BINDING_ID, "ethernet"); + public final static ThingTypeUID THING_TYPE_BLASTER = new ThingTypeUID(BINDING_ID, "blaster"); + + // List of all Channel ids + public final static String CHANNEL_IO = "io"; + + // List of all Channel types + public static final String BLASTER_CHANNEL_TYPE = "Blaster"; + public static final String RECEIVER_CHANNEL_TYPE = "Receiver"; + + // List of possible leds on a IRtrans transceiver + public enum Led { + DEFAULT("D"), + INTERNAL("I"), + EXTERNAL("E"), + ALL("B"), + ONE("1"), + TWO("2"), + THREE("3"), + FOUR("4"), + FIVE("5"), + SIX("6"), + SEVEN("7"), + EIGHT("8"); + + private final String text; + + private Led(final String text) { + this.text = text; + } + + @Override + public String toString() { + return text; + } + + public static Led get(String valueSelectorText) throws IllegalArgumentException { + + for (Led c : Led.values()) { + if (c.text.equals(valueSelectorText)) { + return c; + } + } + + throw new IllegalArgumentException("Not valid value selector"); + } + + } + +} diff --git a/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/factory/IRtransHandlerFactory.java b/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/factory/IRtransHandlerFactory.java new file mode 100644 index 0000000000000..ada051233e3ac --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/factory/IRtransHandlerFactory.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2014-2015 openHAB UG (haftungsbeschraenkt) and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.irtrans.factory; + +import java.util.Collection; + +import org.eclipse.smarthome.config.core.Configuration; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.openhab.binding.irtrans.IRtransBindingConstants; +import org.openhab.binding.irtrans.handler.BlasterHandler; +import org.openhab.binding.irtrans.handler.EthernetBridgeHandler; + +import com.google.common.collect.Lists; + +/** + * The {@link IRtransHandlerFactory} is responsible for creating things and + * thing handlers. + * + * @author Karel Goderis - Initial contribution + * @since 2.1.0 + * + */ +public class IRtransHandlerFactory extends BaseThingHandlerFactory { + + public final static Collection SUPPORTED_THING_TYPES_UIDS = Lists.newArrayList( + IRtransBindingConstants.THING_TYPE_BLASTER, IRtransBindingConstants.THING_TYPE_ETHERNET_BRIDGE); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + public Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration, ThingUID thingUID, + ThingUID bridgeUID) { + if (IRtransBindingConstants.THING_TYPE_ETHERNET_BRIDGE.equals(thingTypeUID)) { + ThingUID ethernetBridgeUID = getEthernetBridgeThingUID(thingTypeUID, thingUID, configuration); + return super.createThing(thingTypeUID, configuration, ethernetBridgeUID, null); + } + if (IRtransBindingConstants.THING_TYPE_BLASTER.equals(thingTypeUID)) { + ThingUID blasterUID = getBlasterUID(thingTypeUID, thingUID, configuration, bridgeUID); + return super.createThing(thingTypeUID, configuration, blasterUID, bridgeUID); + } + throw new IllegalArgumentException( + "The thing type " + thingTypeUID + " is not supported by the IRtrans binding."); + } + + @Override + protected ThingHandler createHandler(Thing thing) { + if (thing.getThingTypeUID().equals(IRtransBindingConstants.THING_TYPE_ETHERNET_BRIDGE)) { + return new EthernetBridgeHandler((Bridge) thing); + } else if (thing.getThingTypeUID().equals(IRtransBindingConstants.THING_TYPE_BLASTER)) { + return new BlasterHandler(thing); + } else { + return null; + } + } + + private ThingUID getEthernetBridgeThingUID(ThingTypeUID thingTypeUID, ThingUID thingUID, + Configuration configuration) { + if (thingUID == null) { + String ipAddress = (String) configuration.get(EthernetBridgeHandler.IP_ADDRESS); + thingUID = new ThingUID(thingTypeUID, ipAddress); + } + return thingUID; + } + + private ThingUID getBlasterUID(ThingTypeUID thingTypeUID, ThingUID thingUID, Configuration configuration, + ThingUID bridgeUID) { + String ledId = (String) configuration.get(BlasterHandler.LED); + + if (thingUID == null) { + thingUID = new ThingUID(thingTypeUID, "Led" + ledId, bridgeUID.getId()); + } + return thingUID; + } +} diff --git a/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/handler/BlasterHandler.java b/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/handler/BlasterHandler.java new file mode 100644 index 0000000000000..c12a506be3562 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/handler/BlasterHandler.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2014-2015 openHAB UG (haftungsbeschraenkt) and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.irtrans.handler; + +import static org.openhab.binding.irtrans.IRtransBindingConstants.CHANNEL_IO; + +import org.apache.commons.lang.StringUtils; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.openhab.binding.irtrans.IRcommand; +import org.openhab.binding.irtrans.IRtransBindingConstants.Led; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link BlasterHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Karel Goderis - Initial contribution + * @since 2.1.0 + * + */ +public class BlasterHandler extends BaseThingHandler implements TransceiverStatusListener { + + // List of Configuration constants + public static final String COMMAND = "command"; + public static final String LED = "led"; + public static final String REMOTE = "remote"; + + private Logger logger = LoggerFactory.getLogger(BlasterHandler.class); + + public BlasterHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + ((EthernetBridgeHandler) getBridge().getHandler()).registerTransceiverStatusListener(this); + } + + @Override + public void handleRemoval() { + ((EthernetBridgeHandler) getBridge().getHandler()).unregisterTransceiverStatusListener(this); + updateStatus(ThingStatus.REMOVED); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + + EthernetBridgeHandler ethernetBridge = (EthernetBridgeHandler) getBridge().getHandler(); + + if (ethernetBridge == null) { + logger.warn("IRtrans Ethernet bridge handler not found. Cannot handle command without bridge."); + return; + } + + if (command instanceof RefreshType) { + // Placeholder for future refinement + } else { + if (channelUID.getId().equals(CHANNEL_IO)) { + if (command instanceof StringType) { + String remoteName = StringUtils.substringBefore(command.toString(), ","); + String irCommandName = StringUtils.substringAfter(command.toString(), ","); + + IRcommand ircommand = new IRcommand(); + ircommand.remote = remoteName; + ircommand.command = irCommandName; + + IRcommand thingCompatibleCommand = new IRcommand(); + thingCompatibleCommand.remote = (String) getConfig().get(REMOTE); + thingCompatibleCommand.command = (String) getConfig().get(COMMAND); + + if (ircommand.matches(thingCompatibleCommand)) { + if (!ethernetBridge.sendIRcommand(ircommand, Led.get((String) getConfig().get(LED)))) { + logger.warn("An error occured whilst sending the infrared command '{}' for Channel '{}'", + ircommand, channelUID); + } + } + } + } + } + } + + @Override + public void onCommandReceived(EthernetBridgeHandler bridge, IRcommand command) { + + logger.debug("Received command {},{} for thing {}", + new Object[] { command.remote, command.command, this.getThing().getUID() }); + + IRcommand thingCompatibleCommand = new IRcommand(); + thingCompatibleCommand.remote = (String) getConfig().get(REMOTE); + thingCompatibleCommand.command = (String) getConfig().get(COMMAND); + + if (command.matches(thingCompatibleCommand)) { + StringType stringType = new StringType(command.remote + "," + command.command); + updateState(new ChannelUID(getThing().getUID(), CHANNEL_IO), stringType); + } + + } + + @Override + public void onBridgeDisconnected(EthernetBridgeHandler bridge) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } + + @Override + public void onBridgeConnected(EthernetBridgeHandler bridge) { + updateStatus(ThingStatus.ONLINE); + } +} diff --git a/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/handler/EthernetBridgeHandler.java b/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/handler/EthernetBridgeHandler.java new file mode 100644 index 0000000000000..ea095cfb44e08 --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/handler/EthernetBridgeHandler.java @@ -0,0 +1,1026 @@ +/** + * Copyright (c) 2014-2015 openHAB UG (haftungsbeschraenkt) and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.irtrans.handler; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.NoConnectionPendingException; +import java.nio.channels.NotYetConnectedException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.bind.DatatypeConverter; + +import org.apache.commons.lang.StringUtils; +import org.eclipse.smarthome.config.core.Configuration; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.openhab.binding.irtrans.IRcommand; +import org.openhab.binding.irtrans.IRtransBindingConstants; +import org.openhab.binding.irtrans.IRtransBindingConstants.Led; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EthernetBridgeHandler} is responsible for handling commands, which + * are sent to one of the channels. + * + * @author Karel Goderis - Initial contribution + * @since 2.1.0 + * + */ +public class EthernetBridgeHandler extends BaseBridgeHandler implements TransceiverStatusListener { + + // List of Configuration constants + public static final String BUFFER_SIZE = "bufferSize"; + public static final String IP_ADDRESS = "ipAddress"; + public static final String IS_LISTENER = "isListener"; + public static final String FIRMWARE_VERSION = "firmwareVersion"; + public static final String LISTENER_PORT = "listenerPort"; + public static final String MODE = "mode"; + public static final String PING_TIME_OUT = "pingTimeOut"; + public static final String PORT_NUMBER = "portNumber"; + public static final String RECONNECT_INTERVAL = "reconnectInterval"; + public static final String REFRESH_INTERVAL = "refreshInterval"; + public static final String RESPONSE_TIME_OUT = "responseTimeOut"; + public static final String COMMAND = "command"; + public static final String LED = "led"; + public static final String REMOTE = "remote"; + + private Logger logger = LoggerFactory.getLogger(EthernetBridgeHandler.class); + + private Selector selector; + private SocketChannel socketChannel; + protected SelectionKey socketChannelKey = null; + protected ServerSocketChannel listenerChannel = null; + protected SelectionKey listenerKey = null; + protected boolean previousConnectionState = false; + private final Lock lock = new ReentrantLock(); + + private List transceiverStatusListeners = new CopyOnWriteArrayList<>(); + + private ScheduledFuture pollingJob; + + // 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 Collection irCommands = new HashSet(); + + public EthernetBridgeHandler(Bridge bridge) { + super(bridge); + // Nothing to do here + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + + if (command instanceof RefreshType) { + // Nothing to do here + } else { + if (channelUID != null) { + Channel channel = this.getThing().getChannel(channelUID.getId()); + if (channel != null) { + Configuration channelConfiguration = channel.getConfiguration(); + if (channel.getAcceptedItemType().equals(IRtransBindingConstants.BLASTER_CHANNEL_TYPE)) { + if (command instanceof StringType) { + String remoteName = StringUtils.substringBefore(command.toString(), ","); + String irCommandName = StringUtils.substringAfter(command.toString(), ","); + + IRcommand ircommand = new IRcommand(); + ircommand.remote = remoteName; + ircommand.command = irCommandName; + + IRcommand thingCompatibleCommand = new IRcommand(); + thingCompatibleCommand.remote = (String) channelConfiguration.get(REMOTE); + thingCompatibleCommand.command = (String) channelConfiguration.get(COMMAND); + + if (ircommand.matches(thingCompatibleCommand)) { + if (sendIRcommand(ircommand, Led.get((String) channelConfiguration.get(LED)))) { + logger.debug("Sent a matching infrared command '{}' for channel '{}'", + command.toString(), channelUID); + } else { + logger.warn( + "An error occured whilst sending the infrared command '{}' for Channel '{}'", + command.toString(), channelUID); + } + } + + } + } + + if (channel.getAcceptedItemType().equals(IRtransBindingConstants.RECEIVER_CHANNEL_TYPE)) { + logger.warn("Receivers can only receive infrared commands, not send them"); + } + } + } + } + } + + @Override + public void initialize() { + logger.debug("Initializing IRtrans Ethernet handler."); + + // register ourselves as a Transceiver Status Listener + registerTransceiverStatusListener(this); + + try { + selector = Selector.open(); + } catch (IOException e) { + logger.error("An exception occurred while registering the selector: '{}'", e.getMessage()); + } + + if (getConfig().get(IP_ADDRESS) != null && getConfig().get(PORT_NUMBER) != null) { + onUpdate(); + } else { + logger.warn("Cannot connect to IRtrans Ethernet device. IP address or port number not set."); + } + + if (getConfig().get(IS_LISTENER) != null) { + configureListener((String) getConfig().get(LISTENER_PORT)); + } + } + + @Override + public void dispose() { + logger.debug("Handler disposed."); + + unregisterTransceiverStatusListener(this); + + disconnect(socketChannel); + + try { + selector.close(); + } catch (IOException e) { + logger.error("An exception occurred while closing the selector: '{}'", e.getMessage()); + } + + if (pollingJob != null && !pollingJob.isCancelled()) { + pollingJob.cancel(true); + pollingJob = null; + } + } + + private void establishConnection() { + lock.lock(); + try { + if (getConfig().get(IP_ADDRESS) != null && getConfig().get(PORT_NUMBER) != null) { + socketChannel = connect((String) getConfig().get(IP_ADDRESS), + ((BigDecimal) getConfig().get(PORT_NUMBER)).intValue()); + try { + Thread.sleep(((BigDecimal) getConfig().get(RESPONSE_TIME_OUT)).intValue()); + } catch (NumberFormatException | InterruptedException e) { + logger.debug("An exception occurred while putting a thread to sleep: '{}'", e.getMessage()); + } + onConnectable(socketChannel); + } + } finally { + lock.unlock(); + } + } + + private synchronized void onUpdate() { + if (pollingJob == null || pollingJob.isCancelled()) { + pollingJob = scheduler.scheduleAtFixedRate(pollingRunnable, 0, + ((BigDecimal) getConfig().get(REFRESH_INTERVAL)).intValue(), TimeUnit.MILLISECONDS); + } + } + + public void onConnectionLost() { + logger.debug("Updating thing status to OFFLINE."); + updateStatus(ThingStatus.OFFLINE); + for (TransceiverStatusListener listener : transceiverStatusListeners) { + listener.onBridgeDisconnected(this); + } + establishConnection(); + } + + public void onConnectionResumed() { + logger.debug("Updating thing status to ONLINE."); + configureTransceiver(socketChannel); + updateStatus(ThingStatus.ONLINE); + for (TransceiverStatusListener listener : transceiverStatusListeners) { + listener.onBridgeConnected(this); + } + } + + private void configureListener(String listenerPort) { + try { + listenerChannel = ServerSocketChannel.open(); + listenerChannel.socket().bind(new InetSocketAddress(Integer.parseInt(listenerPort))); + listenerChannel.configureBlocking(false); + + logger.info("Listening for incoming connections on {}", listenerChannel.getLocalAddress()); + + synchronized (selector) { + selector.wakeup(); + try { + listenerKey = listenerChannel.register(selector, SelectionKey.OP_ACCEPT); + } catch (ClosedChannelException e1) { + logger.error("An exception occurred while registering a selector: '{}'", e1.getMessage()); + } + } + } catch (Exception e3) { + logger.error( + "An exception occurred while creating configuring the listener channel on port number {}: '{}'", + Integer.parseInt(listenerPort), e3.getMessage()); + } + } + + protected void configureTransceiver(SocketChannel socketChannel) { + lock.lock(); + try { + + String putInASCIImode = "ASCI"; + ByteBuffer response = sendCommand(putInASCIImode); + + String getFirmwareVersion = "Aver" + (char) 13; + response = sendCommand(getFirmwareVersion); + + if (response != null) { + String message = stripByteCount(response).split("\0")[0]; + if (message != null) { + if (message.contains("VERSION")) { + logger.info("'{}' matches an IRtrans device with firmware {}", getThing().getUID().toString(), + message); + getConfig().put(FIRMWARE_VERSION, message); + } else { + logger.warn("Received some non-compliant garbage ({})", message); + disconnect(socketChannel); + } + } + } else { + try { + logger.warn("Did not receive an answer from the IRtrans transceiver '{}' - Parsing is skipped", + socketChannel.getRemoteAddress()); + disconnect(socketChannel); + } catch (IOException e1) { + logger.debug("An exception occurred while getting a remote address: '{}'", e1.getMessage()); + } + } + + int numberOfRemotes = 0; + int numberOfRemotesProcessed = 0; + int numberOfRemotesInBatch = 0; + String[] remoteList = getRemoteList(0); + + if (remoteList.length > 0) { + logger.info("The IRtrans device for '{}' supports '{}' remotes", getThing().getUID(), remoteList[1]); + numberOfRemotes = Integer.valueOf(remoteList[1]); + numberOfRemotesInBatch = Integer.valueOf(remoteList[2]); + } + + while (numberOfRemotesProcessed < numberOfRemotes) { + for (int i = 1; i <= numberOfRemotesInBatch; i++) { + String remote = remoteList[2 + i]; + + // get remote commands + String[] commands = getRemoteCommands(remote, 0); + String resultString = new String(); + int numberOfCommands = 0; + int numberOfCommandsInBatch = 0; + int numberOfCommandsProcessed = 0; + + if (commands.length > 0) { + numberOfCommands = Integer.valueOf(commands[1]); + numberOfCommandsInBatch = Integer.valueOf(commands[2]); + numberOfCommandsProcessed = 0; + } + + while (numberOfCommandsProcessed < numberOfCommands) { + for (int j = 1; j <= numberOfCommandsInBatch; j++) { + String command = commands[2 + j]; + resultString = resultString + command; + numberOfCommandsProcessed++; + if (numberOfCommandsProcessed < numberOfCommands) { + resultString = resultString + ", "; + } + } + + if (numberOfCommandsProcessed < numberOfCommands) { + commands = getRemoteCommands(remote, numberOfCommandsProcessed); + if (commands.length == 0) { + break; + } + numberOfCommandsInBatch = Integer.valueOf(commands[2]); + } else { + numberOfCommandsInBatch = 0; + } + + } + + logger.info("The remote '{}' on '{}' supports '{}' commands: {}", + new Object[] { remote, getThing().getUID(), numberOfCommands, resultString }); + + numberOfRemotesProcessed++; + } + + // get next batch + if (numberOfRemotesProcessed < numberOfRemotes) { + remoteList = getRemoteList(numberOfRemotesProcessed); + if (remoteList.length == 0) { + break; + } + numberOfRemotesInBatch = Integer.valueOf(remoteList[2]); + } else { + numberOfRemotesInBatch = 0; + } + + } + + } finally { + lock.unlock(); + } + } + + private String[] getRemoteCommands(String remote, int index) { + + String getCommands = "Agetcommands " + remote + "," + index + (char) 13; + ByteBuffer response = sendCommand(getCommands); + String[] commandList = new String[0]; + + if (response != null) { + String message = stripByteCount(response).split("\0")[0]; + logger.trace("commands returned {}", message); + if (message != null) { + if (message.contains("COMMANDLIST")) { + commandList = message.split(","); + } else { + logger.warn("Received some non-compliant command ({})", message); + disconnect(socketChannel); + } + } + } else { + logger.warn("Did not receive an answer from the IRtrans transceiver for '{}' - Parsing is skipped", + getThing().getUID()); + disconnect(socketChannel); + } + + return commandList; + } + + private String[] getRemoteList(int index) { + + String getRemotes = "Agetremotes " + index + (char) 13; + ByteBuffer response = sendCommand(getRemotes); + String[] remoteList = new String[0]; + + if (response != null) { + String message = stripByteCount(response).split("\0")[0]; + logger.trace("remotes returned {}", message); + if (message != null) { + if (message.contains("REMOTELIST")) { + remoteList = message.split(","); + } else { + logger.warn("Received some non-compliant command ({})", message); + disconnect(socketChannel); + } + } + } else { + logger.warn("Did not receive an answer from the IRtrans transceiver for '{}' - Parsing is skipped", + getThing().getUID()); + disconnect(socketChannel); + } + + return remoteList; + } + + private ByteBuffer sendCommand(String command) { + + if (command != null) { + ByteBuffer byteBuffer = ByteBuffer.allocate(command.getBytes().length); + try { + byteBuffer.put(command.getBytes("ASCII")); + onWritable(byteBuffer, socketChannel); + Thread.sleep(((BigDecimal) getConfig().get(RESPONSE_TIME_OUT)).intValue()); + return onReadable(socketChannel, ((BigDecimal) getConfig().get(BUFFER_SIZE)).intValue()); + } catch (UnsupportedEncodingException | NumberFormatException | InterruptedException e) { + logger.error("An exception occurred while configurting the IRtrans transceiver for '{}': {}", + getThing().getUID(), e.getMessage()); + } + } + + return null; + } + + @SuppressWarnings("null") + private SocketChannel connect(String ipAddress, int portNumber) { + SocketChannel socketChannel = null; + try { + socketChannel = SocketChannel.open(); + } catch (Exception e2) { + logger.error("An exception occurred while connecting to {}:{} : {}", + new Object[] { ipAddress, portNumber, e2.getMessage() }); + } + + try { + socketChannel.socket().setKeepAlive(true); + socketChannel.configureBlocking(false); + } catch (Exception e) { + logger.error("An exception occurred while configuring the connection to '{}:{}' : {}", + new Object[] { ipAddress, portNumber, e.getMessage() }); + } + + while (selector == null) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + logger.error("An exception occurred while putting the Thread to sleep: {}", e.getMessage(), e); + } + } + + synchronized (selector) { + selector.wakeup(); + int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE | SelectionKey.OP_CONNECT; + try { + socketChannelKey = socketChannel.register(selector, interestSet); + } catch (ClosedChannelException e1) { + logger.error("An exception occurred while registering a selector: {}", e1.getMessage()); + } + } + + InetSocketAddress remoteAddress = new InetSocketAddress(ipAddress, portNumber); + + try { + logger.trace("Connecting the channel for {} ", remoteAddress); + socketChannel.connect(remoteAddress); + } catch (Exception e) { + logger.error("An exception occurred while connecting connecting to '{}:{}' : {}", + new Object[] { ipAddress, portNumber, e.getMessage() }); + } + + return socketChannel; + } + + private void disconnect(SocketChannel socketChannel) { + logger.trace("Disconnecting the socket channel '{}'", socketChannel); + try { + socketChannel.close(); + } catch (IOException e) { + logger.warn("An exception occurred while closing the channel '{}': {}", socketChannel, e.getMessage()); + } + } + + protected void onAcceptable(ServerSocketChannel listenerChannel) { + lock.lock(); + try { + synchronized (selector) { + try { + selector.selectNow(); + } catch (IOException e) { + logger.error("An exception occurred while selecting: {}", e.getMessage()); + } + } + + Iterator it = selector.selectedKeys().iterator(); + while (it.hasNext()) { + SelectionKey selKey = it.next(); + it.remove(); + if (selKey.isValid()) { + if (selKey.isAcceptable() && selKey == listenerChannel.keyFor(selector)) { + try { + SocketChannel newChannel = listenerChannel.accept(); + logger.trace("Received a connection request from '{}'", newChannel.getRemoteAddress()); + + try { + newChannel.configureBlocking(false); + } catch (IOException e) { + logger.error("An exception occurred while configuring the channel '{}': {}", newChannel, + e.getMessage()); + } + + synchronized (selector) { + selector.wakeup(); + try { + newChannel.register(selector, newChannel.validOps()); + } catch (ClosedChannelException e1) { + logger.error("An exception occurred while registering a selector: {}", + e1.getMessage()); + } + } + } catch (IOException e) { + logger.error("An exception occurred while accepting a connection on channel '{}': {}", + listenerChannel, e.getMessage()); + } + } + } + } + } finally { + lock.unlock(); + } + } + + protected ByteBuffer onReadable(SocketChannel theChannel, int bufferSize) { + lock.lock(); + try { + + synchronized (selector) { + try { + selector.selectNow(); + } catch (IOException e) { + logger.error("An exception occurred while selecting: {}", e.getMessage()); + } + } + + Iterator it = selector.selectedKeys().iterator(); + while (it.hasNext()) { + SelectionKey selKey = it.next(); + it.remove(); + if (selKey.isValid() && selKey.isReadable()) { + SocketChannel socketChannel = (SocketChannel) selKey.channel(); + + if (socketChannel == theChannel || theChannel == null) { + ByteBuffer readBuffer = ByteBuffer.allocate(bufferSize); + int numberBytesRead = 0; + boolean error = false; + + try { + numberBytesRead = socketChannel.read(readBuffer); + } catch (NotYetConnectedException e) { + logger.warn("The channel '{}' is not yet connected: {}", socketChannel, e.getMessage()); + if (!socketChannel.isConnectionPending()) { + error = true; + } + } catch (IOException e) { + // If some other I/O error occurs + logger.warn("An IO exception occured on chanel '{}': {}", socketChannel, e.getMessage()); + error = true; + } + + if (numberBytesRead == -1) { + error = true; + } + + if (error) { + logger.debug("Disconnecting '{}' because of a socket error", + getThing().getUID().toString()); + disconnect(socketChannel); + } else { + readBuffer.flip(); + return readBuffer; + } + } + } + } + + return null; + } finally { + lock.unlock(); + } + } + + protected void onWritable(ByteBuffer buffer, SocketChannel theChannel) { + lock.lock(); + try { + synchronized (selector) { + try { + selector.selectNow(); + } catch (IOException e) { + logger.error("An exception occurred while selecting: {}", e.getMessage()); + } + } + + Iterator it = selector.selectedKeys().iterator(); + while (it.hasNext()) { + SelectionKey selKey = it.next(); + it.remove(); + if (selKey.isValid() && selKey.isWritable()) { + + SocketChannel socketChannel = (SocketChannel) selKey.channel(); + + if (socketChannel == theChannel || theChannel == null) { + + boolean error = false; + + buffer.rewind(); + try { + logger.trace("Sending '{}' on the channel '{}'->'{}'", + new Object[] { new String(buffer.array()), socketChannel.getLocalAddress(), + socketChannel.getRemoteAddress() }); + socketChannel.write(buffer); + } catch (NotYetConnectedException e) { + logger.warn("The channel '{}' is not yet connected: {}", socketChannel, e.getMessage()); + if (!socketChannel.isConnectionPending()) { + error = true; + } + } catch (ClosedChannelException e) { + // If some other I/O error occurs + logger.warn("The channel for '{}' is closed: {}", socketChannel, e.getMessage()); + error = true; + } catch (IOException e) { + // If some other I/O error occurs + logger.warn("An IO exception occured on chanel '{}': {}", socketChannel, e.getMessage()); + error = true; + } + + if (error) { + disconnect(socketChannel); + } + } + } + } + } finally { + lock.unlock(); + } + } + + protected void onConnectable(SocketChannel theChannel) { + lock.lock(); + try { + synchronized (selector) { + try { + selector.selectNow(); + } catch (IOException e) { + logger.error("An exception occurred while selecting: {}", e.getMessage()); + } + } + + Iterator it = selector.selectedKeys().iterator(); + while (it.hasNext()) { + SelectionKey selKey = it.next(); + it.remove(); + if (selKey.isValid() && selKey.isConnectable()) { + SocketChannel socketChannel = (SocketChannel) selKey.channel(); + + if (socketChannel == theChannel || theChannel == null) { + boolean result = false; + try { + result = socketChannel.finishConnect(); + } catch (NoConnectionPendingException e) { + // this channel is not connected and a connection + // operation has not been initiated + try { + logger.warn("The channel for '{}' has no connection pending: {}", + socketChannel.getRemoteAddress(), e.getMessage()); + } catch (IOException e1) { + logger.debug("An exception occurred while getting a remote address: '{}'", + e1.getMessage()); + } + } catch (ClosedChannelException e) { + // If some other I/O error occurs + logger.warn("The channel for '{}' is closed: {}", socketChannel, e.getMessage()); + } catch (IOException e) { + logger.warn("An IO exception occured on chanel for '{}': {}", socketChannel, + e.getMessage()); + } + + if (!result) { + logger.debug("Disconnecting '{}' because of a socket finish connect error", + getThing().getUID().toString()); + disconnect(socketChannel); + } else { + try { + logger.trace("The channel for '{}' is connected", socketChannel.getRemoteAddress()); + } catch (IOException e1) { + logger.debug("An exception occurred while getting a remote address: '{}'", + e1.getMessage()); + } + } + } + } + } + } finally { + lock.unlock(); + } + } + + protected int getByteCount(ByteBuffer byteBuffer) { + Pattern RESPONSE_PATTERN = Pattern.compile("..(\\d{5}) (.*)", Pattern.DOTALL); + + String response = new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); + response = StringUtils.chomp(response); + + Matcher matcher = RESPONSE_PATTERN.matcher(response); + if (matcher.matches()) { + return Integer.parseInt(matcher.group(1)); + } + + return 0; + } + + protected String stripByteCount(ByteBuffer byteBuffer) { + Pattern RESPONSE_PATTERN = Pattern.compile("..(\\d{5}) (.*)", Pattern.DOTALL); + 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; + } + + public boolean sendIRcommand(IRcommand command, Led led) { + // construct the string we need to send to the IRtrans device + String output = packIRDBCommand(led, command); + + lock.lock(); + try { + ByteBuffer response = sendCommand(output); + + if (response != null) { + String message = stripByteCount(response).split("\0")[0]; + if (message != null && message.contains("RESULT OK")) { + return true; + } else { + logger.debug("Received an unexpected response from the IRtrans transceiver: '{}'", message); + return false; + } + } + } finally { + lock.unlock(); + } + + return false; + } + + protected void parseOKMessage(String message) { + // Nothing to do here + } + + protected void parseHexMessage(String message) { + Pattern HEX_PATTERN = Pattern.compile("RCV_HEX (.*)"); + Matcher matcher = HEX_PATTERN.matcher(message); + + if (matcher.matches()) { + String command = matcher.group(1); + + IRcommand theCommand = null; + for (IRcommand aCommand : irCommands) { + if (aCommand.sequenceToHEXString().equals(command)) { + theCommand = aCommand; + break; + } + } + + if (theCommand != null) { + for (TransceiverStatusListener listener : transceiverStatusListeners) { + listener.onCommandReceived(this, theCommand); + } + } else { + logger.error("{} does not match any know infrared command", command); + } + + } else { + logger.error("{} does not match the infrared message format '{}'", message, matcher.pattern()); + } + } + + protected void parseIRDBMessage(String message) { + Pattern IRDB_PATTERN = Pattern.compile("RCV_COM (.*),(.*),(.*),(.*)"); + Matcher matcher = IRDB_PATTERN.matcher(message); + + if (matcher.matches()) { + IRcommand command = new IRcommand(); + command.remote = matcher.group(1); + command.command = matcher.group(2); + + for (TransceiverStatusListener listener : transceiverStatusListeners) { + listener.onCommandReceived(this, command); + } + + } else { + logger.error("{} does not match the IRDB infrared message format '{}'", message, matcher.pattern()); + } + } + + /** + * "Pack" the infrared command so that it can be sent to the IRTrans device + * + * @param led + * the led + * @param command + * the the command + * @return a string which is the full command to be sent to the device + */ + protected String packIRDBCommand(Led led, IRcommand command) { + String output = new String(); + + output = "Asnd "; + output += command.remote; + output += ","; + output += command.command; + output += ",l"; + output += led.toString(); + + output += "\r\n"; + + return output; + } + + /** + * "Pack" the infrared command so that it can be sent to the IRTrans device + * + * @param led + * the led + * @param command + * the the command + * @return a string which is the full command to be sent to the device + */ + protected String packHexCommand(Led led, IRcommand command) { + String output = new String(); + + output = "Asndhex "; + output += "L"; + output += led.toString(); + + output += ","; + output += "H" + command.toHEXString(); + + output += (char) 13; + + return output; + + } + + protected void onWrite(ByteBuffer buffer, SocketChannel socketChannel) { + onWritable(buffer, socketChannel); + } + + protected void onRead(ByteBuffer byteBuffer, SocketChannel socketChannel) { + try { + logger.trace("Received bytebuffer : '{}'", DatatypeConverter.printHexBinary(byteBuffer.array())); + int byteCount = getByteCount(byteBuffer); + + while (byteCount > 0) { + byte[] message = new byte[byteCount]; + byteBuffer.get(message, 0, byteCount); + logger.trace("Received message : '{}'", DatatypeConverter.printHexBinary(message)); + + String strippedBuffer = stripByteCount(ByteBuffer.wrap(message)); + + if (strippedBuffer != null) { + + String strippedMessage = strippedBuffer.split("\0")[0]; + + // IRTrans devices return "RESULT OK" when it succeeds to emit an + // infrared sequence + if (strippedMessage.contains("RESULT OK")) { + parseOKMessage(strippedMessage); + } + + // IRTrans devices return a string starting with RCV_HEX each time + // it captures an infrared sequence from a remote control + if (strippedMessage.contains("RCV_HEX")) { + parseHexMessage(strippedMessage); + } + + // 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 (strippedMessage.contains("RCV_COM")) { + parseIRDBMessage(strippedMessage); + } + + byteCount = getByteCount(byteBuffer); + + } else { + logger.warn("Received some non-compliant garbage '{}' - Parsing is skipped", + new String(byteBuffer.array())); + } + } + } catch (Exception e) { + logger.error("An exception occurred while reading bytebuffer '{}' : {}", + DatatypeConverter.printHexBinary(byteBuffer.array()), e.getMessage(), e); + } + } + + private Runnable pollingRunnable = new Runnable() { + + @Override + public void run() { + try { + if (socketChannel == null) { + previousConnectionState = false; + onConnectionLost(); + } else { + if (previousConnectionState == false && socketChannel.isConnected()) { + previousConnectionState = true; + onConnectionResumed(); + } + + if (previousConnectionState == true && !socketChannel.isConnected() + && !socketChannel.isConnectionPending()) { + previousConnectionState = false; + onConnectionLost(); + } + + if (!socketChannel.isConnectionPending() && !socketChannel.isConnected()) { + previousConnectionState = false; + logger.debug("Disconnecting '{}' because of a network error", getThing().getUID().toString()); + disconnect(socketChannel); + Thread.sleep(1000 * ((BigDecimal) getConfig().get(RECONNECT_INTERVAL)).intValue()); + establishConnection(); + } + + long stamp = System.currentTimeMillis(); + if (!InetAddress.getByName(((String) getConfig().get(IP_ADDRESS))) + .isReachable(((BigDecimal) getConfig().get(PING_TIME_OUT)).intValue())) { + logger.debug( + "Ping timed out after '{}' milliseconds. Disconnecting '{}' because of a ping timeout", + System.currentTimeMillis() - stamp, getThing().getUID().toString()); + disconnect(socketChannel); + } + + onConnectable(socketChannel); + ByteBuffer buffer = onReadable(socketChannel, + ((BigDecimal) getConfig().get(BUFFER_SIZE)).intValue()); + if (buffer != null && buffer.remaining() > 0) { + onRead(buffer, socketChannel); + } + } + + onAcceptable(listenerChannel); + + } catch (Exception e) { + logger.trace("An exception occurred while polling the transceiver : '{}'", e.getMessage(), e); + } + } + }; + + public boolean registerTransceiverStatusListener(TransceiverStatusListener transceiverStatusListener) { + if (transceiverStatusListener == null) { + throw new NullPointerException("It's not allowed to pass a null BlasterStatusListener."); + } + boolean result = transceiverStatusListeners.add(transceiverStatusListener); + if (result) { + onUpdate(); + } + return result; + } + + public boolean unregisterTransceiverStatusListener(TransceiverStatusListener transceiverStatusListener) { + boolean result = transceiverStatusListeners.remove(transceiverStatusListener); + if (result) { + onUpdate(); + } + return result; + } + + @Override + public void onBridgeDisconnected(EthernetBridgeHandler bridge) { + // Nothing to do here + } + + @Override + public void onBridgeConnected(EthernetBridgeHandler bridge) { + // Nothing to do here + } + + @Override + public void onCommandReceived(EthernetBridgeHandler bridge, IRcommand command) { + + logger.debug("Received infrared command '{},{}' for thing '{}'", + new Object[] { command.remote, command.command, this.getThing().getUID() }); + + for (Channel channel : getThing().getChannels()) { + Configuration channelConfiguration = channel.getConfiguration(); + + if (channel.getAcceptedItemType().equals(IRtransBindingConstants.RECEIVER_CHANNEL_TYPE)) { + IRcommand thingCompatibleCommand = new IRcommand(); + thingCompatibleCommand.remote = (String) channelConfiguration.get(REMOTE); + thingCompatibleCommand.command = (String) channelConfiguration.get(COMMAND); + + if (command.matches(thingCompatibleCommand)) { + StringType stringType = new StringType(command.remote + "," + command.command); + logger.debug("Received a matching infrared command '{}' for channel '{}'", stringType.toString(), + channel.getUID()); + updateState(channel.getUID(), stringType); + } + } + } + } +} \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/handler/TransceiverStatusListener.java b/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/handler/TransceiverStatusListener.java new file mode 100644 index 0000000000000..e4a31abc7461f --- /dev/null +++ b/addons/binding/org.openhab.binding.irtrans/src/main/java/org/openhab/binding/irtrans/handler/TransceiverStatusListener.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2014-2015 openHAB UG (haftungsbeschraenkt) and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.irtrans.handler; + +import org.openhab.binding.irtrans.IRcommand; + +/** + * The {@link TransceiverStatusListener} is interface that is to be implemented + * by all classes that wish to be informed of events happening to a infrared + * transceiver + * + * @author Karel Goderis - Initial contribution + * @since 2.1.0 + * + */ +public interface TransceiverStatusListener { + + /** + * + * Called when the ethernet transceiver/bridge receives an infrared command + * + * @param bridge + * @param command - the infrared command + */ + public void onCommandReceived(EthernetBridgeHandler bridge, IRcommand command); + + /** + * + * Called when the connection with the remote transceiver is lost + * + * @param bridge + */ + public void onBridgeDisconnected(EthernetBridgeHandler bridge); + + /** + * Called when the connection with the remote transceiver is established + * + * @param bridge + */ + public void onBridgeConnected(EthernetBridgeHandler bridge); + +}