diff --git a/addons/binding/org.openhab.binding.atlona/.classpath b/addons/binding/org.openhab.binding.atlona/.classpath new file mode 100644 index 0000000000000..a95e0906ca013 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/addons/binding/org.openhab.binding.atlona/.project b/addons/binding/org.openhab.binding.atlona/.project new file mode 100644 index 0000000000000..d4a6f9ec06d49 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/.project @@ -0,0 +1,33 @@ + + + org.openhab.binding.atlona + + + + + + 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.atlona/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.atlona/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..3145b455dea97 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + Atlona Products + Binding for Atlona PRO3 HDBaseT Matrix switches. + Tim Roberts + diff --git a/addons/binding/org.openhab.binding.atlona/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.atlona/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..622b21b8ccfcb --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/ESH-INF/thing/thing-types.xml @@ -0,0 +1,613 @@ + + + + + + + Atlona Pro3 4x4 HDBaseT Matrix (Model AT-UHD-PRO3-44M) + + + + + + Output Port 1 Channels + + + + Output Port 2 Channels + + + + Output Port 3 Channels + + + + Output Port 4 Channels + + + + Output Port 5 Channels + + + + HDMI Port 5 Mirroring Channels + + + + Volume 1 channels + + + + Volume 2 channels + + + + Volume 3 channels + + + + + + network-address + + IP or Host name of Atlona Pro3 44M Switch + true + + + + User Name to login with if Telnet Login is on + false + true + + + password + + Password to login with if Telnet Login is on + false + true + + + + Interval (in seconds) to poll the actual state of the Matrix + 600 + true + + + + Ping Interval (in seconds) to keep the connection alive + 30 + true + + + + Interval (in seconds) to try to (re)connect to the matrix + 10 + true + + + + + + + + Atlona Pro3 6x6 HDBaseT Matrix (Model AT-UHD-PRO3-66M) + + + + + + Output Port 1 Channels + + + + Output Port 2 Channels + + + + Output Port 3 Channels + + + + Output Port 4 Channels + + + + Output Port 5 Channels + + + + Output Port 6 Channels + + + + Output Port 7 Channels + + + + Output Port 8 Channels + + + + HDMI Port 6 Mirroring Channels + + + + HDMI Port 8 Mirroring Channels + + + + Volume 1 channels + + + + Volume 2 channels + + + + Volume 3 channels + + + + Volume 4 channels + + + + + + network-address + + IP or Host name of Atlona Pro3 66M Switch + true + + + + User Name to login with if Telnet Login is on + false + true + + + password + + Password to login with if Telnet Login is on + false + true + + + + Interval (in seconds) to poll the actual state of the Matrix + 600 + true + + + + Ping Interval (in seconds) to keep the connection alive + 30 + true + + + + Interval (in seconds) to try to (re)connect to the matrix + 10 + true + + + + + + + + Atlona Pro3 8x8 HDBaseT Matrix (Model AT-UHD-PRO3-66M) + + + + + + Output Port 1 Channels + + + + Output Port 2 Channels + + + + Output Port 3 Channels + + + + Output Port 4 Channels + + + + Output Port 5 Channels + + + + Output Port 6 Channels + + + + Output Port 7 Channels + + + + Output Port 8 Channels + + + + Output Port 9 Channels + + + + Output Port 10 Channels + + + + HDMI Port 8 Mirroring Channels + + + + HDMI Port 10 Mirroring Channels + + + + Volume 1 channels + + + + Volume 2 channels + + + + Volume 3 channels + + + + Volume 4 channels + + + + Volume 5 channels + + + + Volume 6 channels + + + + + + network-address + + IP or Host name of Atlona Switch + + + + + User Name to use (if Telnet Login is ON) + + true + + + password + + Password to use (if Telnet Login is ON) + + true + + + + Interval (in seconds) to poll the actual state + 600 + true + + + + Ping Interval (in seconds) to keep the connection alive + 30 + true + + + + Interval (in seconds) to try to (re)connect + 10 + true + + + + + + + + Atlona Pro3 16x16 HDBaseT Matrix (Model AT-UHD-PRO3-1616M) + + + + + + Output Port 1 Channels + + + + Output Port 2 Channels + + + + Output Port 3 Channels + + + + Output Port 4 Channels + + + + Output Port 5 Channels + + + + Output Port 6 Channels + + + + Output Port 7 Channels + + + + Output Port 8 Channels + + + + Output Port 9 Channels + + + + Output Port 10 Channels + + + + Output Port 11 Channels + + + + Output Port 12 Channels + + + + Output Port 13 Channels + + + + Output Port 14 Channels + + + + Output Port 15 Channels + + + + Output Port 16 Channels + + + + Output Port 17 Channels + + + + Output Port 18 Channels + + + + Output Port 19 Channels + + + + Output Port 20 Channels + + + + HDMI Port 17 Mirroring Channels + + + + HDMI Port 18 Mirroring Channels + + + + HDMI Port 19 Mirroring Channels + + + + HDMI Port 20 Mirroring Channels + + + + Volume 1 channels + + + + Volume 2 channels + + + + Volume 3 channels + + + + Volume 4 channels + + + + Volume 5 channels + + + + Volume 6 channels + + + + Volume 7 channels + + + + Volume 8 channels + + + + Volume 9 channels + + + + Volume 10 channels + + + + Volume 11 channels + + + + Volume 12 channels + + + + + + network-address + + IP or Host name of Atlona Pro3 1616M Switch + true + + + + User Name to login with if Telnet Login is on + false + true + + + password + + Password to login with if Telnet Login is on + false + true + + + + Interval (in seconds) to poll the actual state of the Matrix + 600 + true + + + + Ping Interval (in seconds) to keep the connection alive + 30 + true + + + + Interval (in seconds) to try to (re)connect to the matrix + 10 + true + + + + + + + Primary Channels + + + + + + + + + + + + + Output Port Channels + + + + + + + + + + HDMI Port Mirroring Channels + + + + + + + + + Volume channels + + + + + + + + + Switch + + Whether the matrix is on or not + + + Switch + + Whether the front panel buttons are locked or not + + + Switch + + Enables or Disables IR + + + String + + Send a preset command ("saveX", "recallX", "clearX") + + + String + + Send a matrix command ("resetmatrix", "resetports", "allportsX") + + + Switch + + Turns on/off the output port + + + Number + + Sets the output port to the input port + + + Number + + Sets the port to be mirrored on the output port + + + Switch + + Whether the HDMI port mirroring is enabled or not + + + Number + + Sets the volume (in db) of the output port (default: -40db with range from -79db to 15db) + + + + Switch + + Sets the output to muted or not + + diff --git a/addons/binding/org.openhab.binding.atlona/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.atlona/META-INF/MANIFEST.MF new file mode 100644 index 0000000000000..84aa87feefd9e --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/META-INF/MANIFEST.MF @@ -0,0 +1,25 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Atlona Binding +Bundle-SymbolicName: org.openhab.binding.atlona;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.apache.commons.lang, + org.eclipse.smarthome.config.core, + org.eclipse.smarthome.config.discovery, + org.eclipse.smarthome.core.library.types, + org.eclipse.smarthome.core.thing, + org.eclipse.smarthome.core.thing.binding, + org.eclipse.smarthome.core.thing.binding.builder, + org.eclipse.smarthome.core.thing.type, + org.eclipse.smarthome.core.types, + org.openhab.binding.atlona, + org.openhab.binding.atlona.handler, + org.slf4j +Service-Component: OSGI-INF/*.xml +Export-Package: org.openhab.binding.atlona, + org.openhab.binding.atlona.handler diff --git a/addons/binding/org.openhab.binding.atlona/OSGI-INF/AtlonaDiscovery.xml b/addons/binding/org.openhab.binding.atlona/OSGI-INF/AtlonaDiscovery.xml new file mode 100644 index 0000000000000..66220f1f6bd14 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/OSGI-INF/AtlonaDiscovery.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.atlona/OSGI-INF/AtlonaHandlerFactory.xml b/addons/binding/org.openhab.binding.atlona/OSGI-INF/AtlonaHandlerFactory.xml new file mode 100644 index 0000000000000..10876245fd4f6 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/OSGI-INF/AtlonaHandlerFactory.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/addons/binding/org.openhab.binding.atlona/README.md b/addons/binding/org.openhab.binding.atlona/README.md new file mode 100644 index 0000000000000..af69b5351c210 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/README.md @@ -0,0 +1,339 @@ +# Atlona Binding + +This binding integrates Atlona AT-UHD-PRO3 HdBaseT matrix switches [Atlona AT-UHD-PRO3 HdBaseT matrix switches](http://www.atlona.com) into your openHAB installation. + +## Supported Things + +This binding supports the following thing types: + +| Thing | Thing Type | Description | +|---------------|------------|---------------------------------------------------------| +| pro3-44m | Thing | The AT-UHD-PRO3-44M 4x4 HDBaseT matrix. | +| pro3-66m | Thing | The AT-UHD-PRO3-66M 6x6 HDBaseT matrix. | +| pro3-88m | Thing | The AT-UHD-PRO3-88M 8x8 HDBaseT matrix. | +| pro3-1616m | Thing | The AT-UHD-PRO3-1616M 16x16 HDBaseT matrix. | + +## Discovery + +The Atlona AT-UHD-PRO3 switch can be discovered by starting a scan in the Paper UI and then logging into your switch and pressing the "SDDP" button on the "Network" tab. The "SDDP" +(simple device discovery protocol) button will initiate the discovery process. If "Telnet Login" is enabled ("Network" tab from the switch configuration UI), you will need to set the +username and password in the configuration of the newly discovered thing before a connection can be made. + +## Binding configuration + +``` +atlona:pro3-88m:home [ ipAddress="192.168.1.30", userName="me", password="12345", polling=600, ping=30, retryPolling=10 ] +``` + +- ipAddress: Hostname or IP address of the matrix switch +- userName: (optional) the username to login with (only if Telnet Login is enabled) +- password: (optional) the password to login with (only if Telnet Login is enabled) +- polling: (optional) the time (in seconds) to poll the state from the actual switch (default: 600) +- ping: (optional) the time (in seconds) to ping the switch to keep our connection alive (default: 30) +- retryPolling: (optional) the time (in seconds) to retry a connection if the connection has failed (default: 10) + +### username/password + +The userName/password configuration options are optional and are only required if you have your switch set with "Telnet Login" enabled (on the "Network" tab from the switch configuration UI). The user must be a valid user listed on the "Users" tab of the switch configuration UI in this case. + +### polling + +Polling will automatically occur when (re)connecting to the switch to get the initial state of the switch. If you have anything outside of openHAB that can modify the switch state (front panel, IR, telnet session or another automation system), you will likely want to set this setting to a much lower value. + +### ping + +The Atlona switch will time out any IP connection after a specific time (specified by "IP Timeout" on the "Network" tab from the switch configuration UI - 120 by default). The ping setting MUST be lower than that value. If it is higher than the "IP Timeout" value, the switch will timeout our connection and the thing will go OFFLINE (until a reconnect attempt is made). + +## Channels + +| Thing | Channel Type ID | Item Type | Access | Description | +|---------------|----------------------------|--------------|--------|-------------------------------------------------------------------------------------------| +| pro3-44m | primary#power | Switch | RW | Matrix Power Switch | +| pro3-44m | primary#panellock | Switch | RW | Sets the front panel locked or unlocked | +| pro3-44m | primary#irenable | Switch | RW | Enables/Disabled the front panel IR | +| pro3-44m | primary#presetcmd | Switch | W | Sends a preset command ('saveX', 'recallX', 'clearX') - see notes below | +| pro3-44m | primary#matrixcmd | Switch | W | Sends a matrix command ('resetmatrix', 'resetports', 'allportsX') - see notes below | +| pro3-44m | port1#portpower | Switch | RW | Enables/Disables output port #1 | +| pro3-44m | port1#portoutput | Number | RW | Sets output port #1 to the specified input port | +| pro3-44m | port2#portpower | Switch | RW | Enables/Disables output port #2 | +| pro3-44m | port2#portoutput | Number | RW | Sets output port #2 to the specified input port | +| pro3-44m | port3#portpower | Switch | RW | Enables/Disables output port #3 | +| pro3-44m | port3#portoutput | Number | RW | Sets output port #3 to the specified input port | +| pro3-44m | port4#portpower | Switch | RW | Enables/Disables output port #4 | +| pro3-44m | port4#portoutput | Number | RW | Sets output port #4 to the specified input port | +| pro3-44m | port5#portpower | Switch | RW | Enables/Disables output port #5 | +| pro3-44m | port5#portoutput | Number | RW | Sets output port #5 to the specified input port | +| pro3-44m | mirror5#portmirrorenabled | Number | RW | Sets hdmi port #5 to enable/disable port mirroring | +| pro3-44m | mirror5#portmirror | Number | RW | Sets hdmi port #5 to mirror the specified output port (if enabled) | +| pro3-44m | volume1#volume | Number | RW | Sets the volume of audio port #1 to the specified decibel level (between -79db to +15db) | +| pro3-44m | volume1#volumemute | Switch | RW | Mutes/Unmutes audio port #1 | +| pro3-44m | volume2#volume | Number | RW | Sets the volume of audio port #2 to the specified decibel level (between -79db to +15db) | +| pro3-44m | volume2#volumemute | Switch | RW | Mutes/Unmutes audio port #2 | +| pro3-44m | volume3#volume | Number | RW | Sets the volume of audio port #3 to the specified decibel level (between -79db to +15db) | +| pro3-44m | volume3#volumemute | Switch | RW | Mutes/Unmutes audio port #3 | +| | | | | | +| pro3-66m | ALL OF THE pro3-44M channels (except different mirror settings) | +| pro3-66m | port6#portpower | Switch | RW | Enables/Disables output port #6 | +| pro3-66m | port6#portoutput | Number | RW | Sets output port #6 to the specified input port | +| pro3-66m | port7#portpower | Switch | RW | Enables/Disables output port #7 | +| pro3-66m | port7#portoutput | Number | RW | Sets output port #7 to the specified input port | +| pro3-66m | port8#portpower | Switch | RW | Enables/Disables output port #8 | +| pro3-66m | port8#portoutput | Number | RW | Sets output port #8 to the specified input port | +| pro3-66m | mirror6#portmirrorenabled | Number | RW | Sets hdmi port #6 to enable/disable port mirroring | +| pro3-66m | mirror6#portmirror | Number | RW | Sets hdmi port #6 to mirror the specified output port (if enabled) | +| pro3-66m | mirror8#portmirrorenabled | Number | RW | Sets hdmi port #8 to enable/disable port mirroring | +| pro3-66m | mirror8#portmirror | Number | RW | Sets hdmi port #8 to mirror the specified output port (if enabled) | +| pro3-66m | volume4#volume | Number | RW | Sets the volume of audio port #4 to the specified decibel level (between -79db to +15db) | +| pro3-66m | volume4#volumemute | Switch | RW | Mutes/Unmutes audio port #4 | +| | | | | | +| pro3-88m | ALL OF THE pro3-66M channels (except different mirror settings) | +| pro3-88m | port9#portpower | Switch | RW | Enables/Disables output port #9 | +| pro3-88m | port9#portoutput | Number | RW | Sets output port #9 to the specified input port | +| pro3-88m | port10#portpower | Switch | RW | Enables/Disables output port #10 | +| pro3-88m | port10#portoutput | Number | RW | Sets output port #10 to the specified input port | +| pro3-88m | mirror8#portmirrorenabled | Number | RW | Sets hdmi port #8 to enable/disable port mirroring | +| pro3-88m | mirror8#portmirror | Number | RW | Sets hdmi port #8 to mirror the specified output port (if enabled) | +| pro3-88m | mirror10#portmirrorenabled | Number | RW | Sets hdmi port #10 to enable/disable port mirroring | +| pro3-88m | mirror10#portmirror | Number | RW | Sets hdmi port #10 to mirror the specified output port (if enabled) | +| pro3-88m | volume5#volume | Number | RW | Sets the volume of audio port #5 to the specified decibel level (between -79db to +15db) | +| pro3-88m | volume5#volumemute | Switch | RW | Mutes/Unmutes audio port #5 | +| pro3-88m | volume6#volume | Number | RW | Sets the volume of audio port #6 to the specified decibel level (between -79db to +15db) | +| pro3-88m | volume6#volumemute | Switch | RW | Mutes/Unmutes audio port #6 | +| | | | | | +| pro3-1616m | ALL OF THE pro3-88M channels (except different mirror settings) | +| pro3-1616m | port11#portpower | Switch | RW | Enables/Disables output port #11 | +| pro3-1616m | port11#portoutput | Number | RW | Sets output port #11 to the specified input port | +| pro3-1616m | port12#portpower | Switch | RW | Enables/Disables output port #12 | +| pro3-1616m | port12#portoutput | Number | RW | Sets output port #12 to the specified input port | +| pro3-1616m | port13#portpower | Switch | RW | Enables/Disables output port #13 | +| pro3-1616m | port13#portoutput | Number | RW | Sets output port #13 to the specified input port | +| pro3-1616m | port14#portpower | Switch | RW | Enables/Disables output port #14 | +| pro3-1616m | port14#portoutput | Number | RW | Sets output port #14 to the specified input port | +| pro3-1616m | port15#portpower | Switch | RW | Enables/Disables output port #15 | +| pro3-1616m | port15#portoutput | Number | RW | Sets output port #15 to the specified input port | +| pro3-1616m | port16#portpower | Switch | RW | Enables/Disables output port #16 | +| pro3-1616m | port16#portoutput | Number | RW | Sets output port #16 to the specified input port | +| pro3-1616m | port17#portpower | Switch | RW | Enables/Disables output port #17 | +| pro3-1616m | port17#portoutput | Number | RW | Sets output port #17 to the specified input port | +| pro3-1616m | port18#portpower | Switch | RW | Enables/Disables output port #18 | +| pro3-1616m | port18#portoutput | Number | RW | Sets output port #18 to the specified input port | +| pro3-1616m | port19#portpower | Switch | RW | Enables/Disables output port #19 | +| pro3-1616m | port19#portoutput | Number | RW | Sets output port #19 to the specified input port | +| pro3-1616m | port20#portpower | Switch | RW | Enables/Disables output port #20 | +| pro3-1616m | port20#portoutput | Number | RW | Sets output port #20 to the specified input port | +| pro3-1616m | mirror17#portmirrorenabled | Number | RW | Sets hdmi port #17 to enable/disable port mirroring | +| pro3-1616m | mirror17#portmirror | Number | RW | Sets hdmi port #17 to mirror the specified output port (if enabled) | +| pro3-1616m | mirror18#portmirrorenabled | Number | RW | Sets hdmi port #18 to enable/disable port mirroring | +| pro3-1616m | mirror18#portmirror | Number | RW | Sets hdmi port #18 to mirror the specified output port (if enabled) | +| pro3-1616m | mirror19#portmirrorenabled | Number | RW | Sets hdmi port #19 to enable/disable port mirroring | +| pro3-1616m | mirror19#portmirror | Number | RW | Sets hdmi port #19 to mirror the specified output port (if enabled) | +| pro3-1616m | mirror20#portmirrorenabled | Number | RW | Sets hdmi port #20 to enable/disable port mirroring | +| pro3-1616m | mirror20#portmirror | Number | RW | Sets hdmi port #20 to mirror the specified output port (if enabled) | +| pro3-1616m | volume7#volume | Number | RW | Sets the volume of audio port #7 to the specified decibel level (between -79db to +15db) | +| pro3-1616m | volume7#volumemute | Switch | RW | Mutes/Unmutes audio port #7 | +| pro3-1616m | volume8#volume | Number | RW | Sets the volume of audio port #8 to the specified decibel level (between -79db to +15db) | +| pro3-1616m | volume8#volumemute | Switch | RW | Mutes/Unmutes audio port #8 | +| pro3-1616m | volume9#volume | Number | RW | Sets the volume of audio port #9 to the specified decibel level (between -79db to +15db) | +| pro3-1616m | volume9#volumemute | Switch | RW | Mutes/Unmutes audio port #9 | +| pro3-1616m | volume10#volume | Number | RW | Sets the volume of audio port #10 to the specified decibel level (between -79db to +15db) | +| pro3-1616m | volume10#volumemute | Switch | RW | Mutes/Unmutes audio port #10 | +| pro3-1616m | volume11#volume | Number | RW | Sets the volume of audio port #11 to the specified decibel level (between -79db to +15db) | +| pro3-1616m | volume11#volumemute | Switch | RW | Mutes/Unmutes audio port #11 | +| pro3-1616m | volume12#volume | Number | RW | Sets the volume of audio port #12 to the specified decibel level (between -79db to +15db) | +| pro3-1616m | volume12#volumemute | Switch | RW | Mutes/Unmutes audio port #12 | + +### presetcmd + +The presetcmd channel will take the following commands: + +| Command | Description | +|---------|-------------| +| saveX | Saves the current input/output to preset X | +| recallX | Sets the input/output to preset X | +| clearX | Clears the preset X | + +Note: if X doesn't exist - nothing will occur. The # of presets allowed depends on the firmware you are using (5 presets up to rev 13, 10 for rev 14 and above). + +### matrixcmd + +The matrixcmd channel will take the following commands: + +| Command | Description | +|---------|-------------| +| resetmatrix | Resets the matrix back to it's default values (USE WITH CARE!). Note: some firmware upgrades require a resetmatrix after installing. | +| resetports | Resets the ports back to their default values (outputX=inputX) | +| allportsX | Sets all the output ports to the input port X | + +Note: if X doesn't exist - nothing will occur. The # of presets allowed depends on the firmware you are using (5 presets up to rev 13, 10 for rev 14 and above). + +## Changes/Warnings + +As of firmware 1.6.03 (rev 13), there are three issues on Atlona firmware (I've notified them on these issues): + +- clearX command does not work. The TCP/IP command "ClearX" as specified in Atlona's protocol will ALWAYS return a "Command Failed". Please avoid this channel until atlona releases a new firmware. +- There is no way to query what the current status is of: panellock, and irenable. This addon simply assumes that panellock is off and irenable is on at startup. +- If you make a change in the switches UI that requires a reboot (mainly changing any of the settings on the "Network" tab in the switch configuration UI), this addon's connection will be inconsistently closed at different times. The thing will go OFFLINE and then back ONLINE when the reconnect attempt is made - and then it starts all over again. Please make sure you reboot as soon as possible when the switch UI notifies you. +- a bug in the firmware will sometimes cause memory presets to disappear after a reboot + +As of firmware 1.6.8 (rev 14), +- The "clearX" command has been fixed and works now. +- The number of presets have increased to 10 +- If telnet mode is enabled, you must use the admin username/password to issue a matrixreset + + +## Example + +### Things + +Here is an example with minimal configuration parameters (using default values with no telnet login): + +``` +atlona:pro3-88m:home [ ipAddress="192.168.1.30" ] +``` + +Here is another example with minimal configuration parameters (using default values with telnet login): + +``` +atlona:pro3-88m:home [ ipAddress="192.168.1.30", userName="me", password="12345" ] +``` + +Here is a full configuration example: + +``` +atlona:pro3-88m:home [ ipAddress="192.168.1.30", userName="me", password="12345", polling=600, ping=30, retryPolling=10 ] +``` + +### Items + +Here is an example of items for the AT-UHD-PRO33-88M: + +``` +Switch Atlona_Power "Power" { channel = "atlona:pro3-88m:home:primary#power" } +Switch Atlona_PanelLock "Panel Lock" { channel = "atlona:pro3-88m:home:primary#panellock" } +Switch Atlona_Presets "Preset Command" { channel = "atlona:pro3-88m:home:primary#presetcmd" } +Switch Atlona_IRLock "IR Lock" { channel = "atlona:pro3-88m:home:primary#irenable" } +Switch Atlona_PortPower1 "Port Power 1" { channel = "atlona:pro3-88m:home:port1#power" } +Switch Atlona_PortPower2 "Port Power 2" { channel = "atlona:pro3-88m:home:port2#power" } +Switch Atlona_PortPower3 "Port Power 3" { channel = "atlona:pro3-88m:home:port3#power" } +Switch Atlona_PortPower4 "Port Power 4" { channel = "atlona:pro3-88m:home:port4#power" } +Switch Atlona_PortPower5 "Port Power 5" { channel = "atlona:pro3-88m:home:port5#power" } +Switch Atlona_PortPower6 "Port Power 6" { channel = "atlona:pro3-88m:home:port6#power" } +Switch Atlona_PortPower7 "Port Power 7" { channel = "atlona:pro3-88m:home:port7#power" } +Switch Atlona_PortPower8 "Port Power 8" { channel = "atlona:pro3-88m:home:port8#power" } +Switch Atlona_PortPower9 "Port Power 9" { channel = "atlona:pro3-88m:home:port9#power" } +Switch Atlona_PortPower10 "Port Power 10" { channel = "atlona:pro3-88m:home:port10#power" } +Number Atlona_PortOutput1 "Living Room [MAP(atlonainputports.map):%s]" { channel = "atlona:pro3-88m:home:port1#portoutput" } +Number Atlona_PortOutput2 "Master Bed [MAP(atlonainputports.map):%s]" { channel = "atlona:pro3-88m:home:port2#portoutput" } +Number Atlona_PortOutput3 "Kitchen [MAP(atlonainputports.map):%s]" { channel = "atlona:pro3-88m:home:port3#portoutput" } +Number Atlona_PortOutput4 "Output 4 [MAP(atlonainputports.map):%s]" { channel = "atlona:pro3-88m:home:port4#portoutput" } +Number Atlona_PortOutput5 "Output 5 [MAP(atlonainputports.map):%s]" { channel = "atlona:pro3-88m:home:port5#portoutput" } +Number Atlona_PortOutput6 "Output 6 [MAP(atlonainputports.map):%s]" { channel = "atlona:pro3-88m:home:port6#portoutput" } +Number Atlona_PortOutput7 "Output 7 [MAP(atlonainputports.map):%s]" { channel = "atlona:pro3-88m:home:port7#portoutput" } +Number Atlona_PortOutput8 "Output 8 [MAP(atlonainputports.map):%s]" { channel = "atlona:pro3-88m:home:port8#portoutput" } +Number Atlona_PortOutput9 "Output 9 [MAP(atlonainputports.map):%s]" { channel = "atlona:pro3-88m:home:port9#portoutput" } +Number Atlona_PortOutput10 "Output 10 [MAP(atlonainputports.map):%s]" { channel = "atlona:pro3-88m:home:port10#portoutput" } +Number Atlona_PortMirror8 "Hdmi Mirror 8 [MAP(atlonaoutputports.map):%s]" { channel = "atlona:pro3-88m:home:mirror8#portmirror" } +Number Atlona_PortMirror10 "Hdmi Mirror 10 [MAP(atlonaoutputports.map):%s]" { channel = "atlona:pro3-88m:home:mirror10#portmirror" } +Number Atlona_Volume1 "Volume 1 [%s db]" { channel = "atlona:pro3-88m:home:volume1#volume" } +Number Atlona_Volume2 "Volume 2 [%s db]" { channel = "atlona:pro3-88m:home:volume2#volume" } +Number Atlona_Volume3 "Volume 3 [%s db]" { channel = "atlona:pro3-88m:home:volume3#volume" } +Number Atlona_Volume4 "Volume 4 [%s db]" { channel = "atlona:pro3-88m:home:volume4#volume" } +Number Atlona_Volume5 "Volume 5 [%s db]" { channel = "atlona:pro3-88m:home:volume5#volume" } +Number Atlona_Volume6 "Volume 6 [%s db]" { channel = "atlona:pro3-88m:home:volume6#volume" } +Switch Atlona_VolumeMute1 "Mute 1" { channel = "atlona:pro3-88m:home:volume1#volumemute" } +Switch Atlona_VolumeMute2 "Mute 2" { channel = "atlona:pro3-88m:home:volume1#volumemute" } +Switch Atlona_VolumeMute3 "Mute 3" { channel = "atlona:pro3-88m:home:volume1#volumemute" } +Switch Atlona_VolumeMute4 "Mute 4" { channel = "atlona:pro3-88m:home:volume1#volumemute" } +Switch Atlona_VolumeMute5 "Mute 5" { channel = "atlona:pro3-88m:home:volume1#volumemute" } +Switch Atlona_VolumeMute6 "Mute 6" { channel = "atlona:pro3-88m:home:volume1#volumemute" } +``` + +### SiteMap + +``` +sitemap demo label="Main Menu" +{ + Frame label="Atlona" { + Text label="Device" { + Switch item=Atlona_Power + Switch item=Atlona_PanelLock + Switch item=Atlona_IRLock + Text item=Atlona_Presets + } + Text label="Ports" { + Switch item=Atlona_PortPower1 + Switch item=Atlona_PortPower2 + Switch item=Atlona_PortPower3 + Switch item=Atlona_PortPower4 + Switch item=Atlona_PortPower5 + Switch item=Atlona_PortPower6 + Switch item=Atlona_PortPower7 + Switch item=Atlona_PortPower8 + Switch item=Atlona_PortPower9 + Switch item=Atlona_PortPower10 + Selection item=Atlona_PortOutput1 mappings=[1="CableBox",2="BluRay Player",3="Roku",4="Apple TV",5="Input 5",6="Input 6",7="Input 7",8="Input 8"] + Selection item=Atlona_PortOutput2 mappings=[1="CableBox",2="BluRay Player",3="Roku",4="Apple TV",5="Input 5",6="Input 6",7="Input 7",8="Input 8"] + Selection item=Atlona_PortOutput3 mappings=[1="CableBox",2="BluRay Player",3="Roku",4="Apple TV",5="Input 5",6="Input 6",7="Input 7",8="Input 8"] + Selection item=Atlona_PortOutput4 mappings=[1="CableBox",2="BluRay Player",3="Roku",4="Apple TV",5="Input 5",6="Input 6",7="Input 7",8="Input 8"] + Selection item=Atlona_PortOutput5 mappings=[1="CableBox",2="BluRay Player",3="Roku",4="Apple TV",5="Input 5",6="Input 6",7="Input 7",8="Input 8"] + Selection item=Atlona_PortOutput6 mappings=[1="CableBox",2="BluRay Player",3="Roku",4="Apple TV",5="Input 5",6="Input 6",7="Input 7",8="Input 8"] + Selection item=Atlona_PortOutput7 mappings=[1="CableBox",2="BluRay Player",3="Roku",4="Apple TV",5="Input 5",6="Input 6",7="Input 7",8="Input 8"] + Selection item=Atlona_PortOutput8 mappings=[1="CableBox",2="BluRay Player",3="Roku",4="Apple TV",5="Input 5",6="Input 6",7="Input 7",8="Input 8"] visibility=[Atlona_PortMirror8==0] + Selection item=Atlona_PortOutput9 mappings=[1="CableBox",2="BluRay Player",3="Roku",4="Apple TV",5="Input 5",6="Input 6",7="Input 7",8="Input 8"] + Selection item=Atlona_PortOutput10 mappings=[1="CableBox",2="BluRay Player",3="Roku",4="Apple TV",5="Input 5",6="Input 6",7="Input 7",8="Input 8"] visibility=[Atlona_PortMirror10==0] + Selection item=Atlona_PortMirror8 mappings=[0="None",1="Living Room",2="Master Bed",3="Kitchen",4="Output 4",5="Output 5",6="Output 6",7="Output 7",9="Output 9"] + Selection item=Atlona_PortMirror10 mappings=[0="None",1="Living Room",2="Master Bed",3="Kitchen",4="Output 4",5="Output 5",6="Output 6",7="Output 7",9="Output 9"] + } + Text label="Audio" { + Setpoint item=Atlona_Volume1 minValue=-79 maxValue=15 + Setpoint item=Atlona_Volume2 minValue=-79 maxValue=15 + Setpoint item=Atlona_Volume3 minValue=-79 maxValue=15 + Setpoint item=Atlona_Volume4 minValue=-79 maxValue=15 + Setpoint item=Atlona_Volume5 minValue=-79 maxValue=15 + Setpoint item=Atlona_Volume6 minValue=-79 maxValue=15 + Switch item=Atlona_VolumeMute1 + Switch item=Atlona_VolumeMute2 + Switch item=Atlona_VolumeMute3 + Switch item=Atlona_VolumeMute4 + Switch item=Atlona_VolumeMute5 + Switch item=Atlona_VolumeMute6 + } + } +} +``` + +# Transformation Maps + +The following is some example transformation maps you can create. Be sure they are in sync with the mappings above. + +### atlonainputports.map + +``` +1=CableBox +2=BluRay Player +3=Roku +4=Apple TV +5=Input 5 +6=Input 6 +7=Input 7 +8=Input 8 +-=- +NULL=- +``` + +### atlonaoutputports.map + +``` +1=Living Room +2=Master Bed +3=Kitchen +4=Output 4 +5=Output 5 +6=Output 6 +7=Output 7 +8=Output 8 +9=Output 9 +10=Output 10 +-=- +NULL=- +``` \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.atlona/build.properties b/addons/binding/org.openhab.binding.atlona/build.properties new file mode 100644 index 0000000000000..66e21b90751a7 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/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.atlona/pom.xml b/addons/binding/org.openhab.binding.atlona/pom.xml new file mode 100644 index 0000000000000..052460ad528a0 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/pom.xml @@ -0,0 +1,19 @@ + + + + 4.0.0 + + + org.openhab.binding + pom + 2.1.0-SNAPSHOT + + + org.openhab.binding + org.openhab.binding.atlona + 2.1.0-SNAPSHOT + + Atlona Binding + eclipse-plugin + + diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/AtlonaBindingConstants.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/AtlonaBindingConstants.java new file mode 100644 index 0000000000000..964a144a5640e --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/AtlonaBindingConstants.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2017 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.atlona; + +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link AtlonaBinding} class defines common constants, which are used across the whole binding. + * + * @author Tim Roberts + */ +public class AtlonaBindingConstants { + + /** + * The binding identifier for atlona + */ + public static final String BINDING_ID = "atlona"; + + /** + * Thing ID for the AT-UHD-PRO3-44m (4x4 hdbaset matrix) + */ + public final static ThingTypeUID THING_TYPE_PRO3_44M = new ThingTypeUID(BINDING_ID, "pro3-44m"); + + /** + * Thing ID for the AT-UHD-PRO3-66m (6x6 hdbaset matrix) + */ + public final static ThingTypeUID THING_TYPE_PRO3_66M = new ThingTypeUID(BINDING_ID, "pro3-66m"); + + /** + * Thing ID for the AT-UHD-PRO3-88m (8x8 hdbaset matrix) + */ + public final static ThingTypeUID THING_TYPE_PRO3_88M = new ThingTypeUID(BINDING_ID, "pro3-88m"); + + /** + * Thing ID for the AT-UHD-PRO3-1616m (16x16 hdbaset matrix) + */ + public final static ThingTypeUID THING_TYPE_PRO3_1616M = new ThingTypeUID(BINDING_ID, "pro3-1616m"); + +} diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/discovery/AtlonaDiscovery.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/discovery/AtlonaDiscovery.java new file mode 100644 index 0000000000000..32ae5491df43d --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/discovery/AtlonaDiscovery.java @@ -0,0 +1,268 @@ +/** + * Copyright (c) 2010-2017 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.atlona.discovery; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.net.NetworkInterface; +import java.net.SocketTimeoutException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryResult; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.openhab.binding.atlona.AtlonaBindingConstants; +import org.openhab.binding.atlona.internal.pro3.AtlonaPro3Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableSet; + +/** + * Discovery class for the Atlona PRO3 line. The PRO3 line uses SDDP (simple device discovery protocol) for discovery + * (similar to UPNP but defined by Control4). The user should start the discovery process in openhab and then log into + * the switch, go to the Network options and press the SDDP button (which initiates the SDDP conversation). + * + * @author Tim Roberts + */ +public class AtlonaDiscovery extends AbstractDiscoveryService { + + private Logger logger = LoggerFactory.getLogger(AtlonaDiscovery.class); + + /** + * Address SDDP broadcasts on + */ + private static final String SDDP_ADDR = "239.255.255.250"; + + /** + * Port number SDDP uses + */ + private static final int SDDP_PORT = 1902; + + /** + * SDDP packet should be only 512 in size - make it 600 to give us some room + */ + private static final int BUFFER_SIZE = 600; + + /** + * Socket read timeout (in ms) - allows us to shutdown the listening every TIMEOUT + */ + private static final int TIMEOUT = 1000; + + /** + * Whether we are currently scanning or not + */ + private boolean _scanning; + + /** + * The {@link ExecutorService} to run the listening threads on. + */ + private ExecutorService _executorService = null; + + /** + * Constructs the discovery class using the thing IDs that we can discover. + */ + public AtlonaDiscovery() { + super(ImmutableSet.of(AtlonaBindingConstants.THING_TYPE_PRO3_44M, AtlonaBindingConstants.THING_TYPE_PRO3_66M, + AtlonaBindingConstants.THING_TYPE_PRO3_88M, AtlonaBindingConstants.THING_TYPE_PRO3_1616M), 30, false); + } + + /** + * {@inheritDoc} + * + * Starts the scan. This discovery will: + * + * The process will continue until {@link #stopScan()} is called. + */ + @Override + protected void startScan() { + if (_executorService != null) { + stopScan(); + } + + logger.debug("Starting Discovery"); + + try { + final InetAddress addr = InetAddress.getByName(SDDP_ADDR); + final List networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); + + _executorService = Executors.newFixedThreadPool(networkInterfaces.size()); + _scanning = true; + for (final NetworkInterface netint : networkInterfaces) { + + _executorService.execute(new Runnable() { + @Override + public void run() { + try { + MulticastSocket multiSocket = new MulticastSocket(SDDP_PORT); + multiSocket.setSoTimeout(TIMEOUT); + multiSocket.setNetworkInterface(netint); + multiSocket.joinGroup(addr); + + while (_scanning) { + DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE); + try { + multiSocket.receive(receivePacket); + + String message = new String(receivePacket.getData()).trim(); + if (message != null && message.length() > 0) { + messageReceive(message); + } + } catch (SocketTimeoutException e) { + // ignore + } + } + + multiSocket.close(); + } catch (Exception e) { + if (!e.getMessage().contains("No IP addresses bound to interface")) { + logger.debug("Error getting ip addresses: {}", e.getMessage(), e); + } + } + } + }); + } + } catch (IOException e) { + logger.debug("Error getting ip addresses: {}", e.getMessage(), e); + } + + } + + /** + * SDDP message has the following format + * + *
+     * NOTIFY ALIVE SDDP/1.0
+     * From: "192.168.1.30:1902"
+     * Host: "AT-UHD-PRO3-88M_B898B0030F4D"
+     * Type: "AT-UHD-PRO3-88M"
+     * Max-Age: 1800
+     * Primary-Proxy: "avswitch"
+     * Proxies: "avswitch"
+     * Manufacturer: "Atlona"
+     * Model: "AT-UHD-PRO3-88M"
+     * Driver: "avswitch_Atlona_AT-UHD-PRO3-88M_IP.c4i"
+     * Config-URL: "http://192.168.1.30/"
+     * 
+ * + * First parse the manufacturer, host, model and IP address from the message. For the "Host" field, we parse out the + * serial #. For the From field, we parse out the IP address (minus the port #). If we successfully found all four + * and the manufacturer is "Atlona" and it's a model we recognize, we then create our thing from it. + * + * @param message possibly null, possibly empty SDDP message + */ + private void messageReceive(String message) { + + if (message == null || message.trim().length() == 0) { + return; + } + + String host = null; + String model = null; + String from = null; + String manufacturer = null; + + for (String msg : message.split("\r\n")) { + int idx = msg.indexOf(':'); + if (idx > 0) { + String name = msg.substring(0, idx); + + if (name.equalsIgnoreCase("Host")) { + host = msg.substring(idx + 1).trim().replaceAll("\"", ""); + int sep = host.indexOf('_'); + if (sep >= 0) { + host = host.substring(sep + 1); + } + } else if (name.equalsIgnoreCase("Model")) { + model = msg.substring(idx + 1).trim().replaceAll("\"", ""); + } else if (name.equalsIgnoreCase("Manufacturer")) { + manufacturer = msg.substring(idx + 1).trim().replaceAll("\"", ""); + } else if (name.equalsIgnoreCase("From")) { + from = msg.substring(idx + 1).trim().replaceAll("\"", ""); + int sep = from.indexOf(':'); + if (sep >= 0) { + from = from.substring(0, sep); + } + } + } + + } + + if (!"Atlona".equalsIgnoreCase(manufacturer)) { + return; + } + + if (host != null && model != null && from != null) { + ThingTypeUID typeId = null; + if (model.equalsIgnoreCase("AT-UHD-PRO3-44M")) { + typeId = AtlonaBindingConstants.THING_TYPE_PRO3_44M; + } else if (model.equalsIgnoreCase("AT-UHD-PRO3-66M")) { + typeId = AtlonaBindingConstants.THING_TYPE_PRO3_66M; + } else if (model.equalsIgnoreCase("AT-UHD-PRO3-88M")) { + typeId = AtlonaBindingConstants.THING_TYPE_PRO3_88M; + } else if (model.equalsIgnoreCase("AT-UHD-PRO3-1616M")) { + typeId = AtlonaBindingConstants.THING_TYPE_PRO3_1616M; + } else { + logger.warn("Unknown model #: {}"); + } + + if (typeId != null) { + logger.debug("Creating binding for {} ({})", model, from); + ThingUID j = new ThingUID(typeId, host); + + Map properties = new HashMap<>(1); + properties.put(AtlonaPro3Config.IpAddress, from); + DiscoveryResult result = DiscoveryResultBuilder.create(j).withProperties(properties) + .withLabel(model + " (" + from + ")").build(); + thingDiscovered(result); + } + } + } + + /** + * {@inheritDoc} + * + * Stops the discovery scan. We set {@link #_scanning} to false (allowing the listening threads to end naturally + * within {@link #TIMEOUT) * 5 time then shutdown the {@link #_executorService} + */ + @Override + protected synchronized void stopScan() { + super.stopScan(); + if (_executorService == null) { + return; + } + + _scanning = false; + + try { + _executorService.awaitTermination(TIMEOUT * 5, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + } + _executorService.shutdown(); + _executorService = null; + } +} diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/handler/AtlonaCapabilities.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/handler/AtlonaCapabilities.java new file mode 100644 index 0000000000000..a9f9e988b35de --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/handler/AtlonaCapabilities.java @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2010-2017 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.atlona.handler; + +/** + * Any model specific capabilities class should inherit from this base class. Currently doesn't provide any generic + * functionality. + * + * @author Tim Roberts + */ +public abstract class AtlonaCapabilities { + +} diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/handler/AtlonaHandler.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/handler/AtlonaHandler.java new file mode 100644 index 0000000000000..f79e3bc752309 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/handler/AtlonaHandler.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2017 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.atlona.handler; + +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; + +/** + * This abstract class should be the base class for any Atlona product line handler. + * + * @author Tim Roberts + */ +public abstract class AtlonaHandler extends BaseThingHandler { + + /** + * The model specific capabilities + */ + private final C _capabilities; + + /** + * Constructs the handler from the specified thing and capabilities + * + * @param thing a non-null {@link org.eclipse.smarthome.core.thing.Thing} + * @param capabilities a non-null {@link org.openhab.binding.atlona.handler.AtlonaCapabilities} + */ + public AtlonaHandler(Thing thing, C capabilities) { + super(thing); + + if (capabilities == null) { + throw new IllegalArgumentException("capabilities cannot be null"); + } + _capabilities = capabilities; + } + + /** + * Returns the model specific capabilities + * + * @return a non-null {@link org.openhab.binding.atlona.handler.AtlonaCapabilities} + */ + protected C getCapabilities() { + return _capabilities; + } +} diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/AtlonaHandlerCallback.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/AtlonaHandlerCallback.java new file mode 100644 index 0000000000000..6a52406a7784d --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/AtlonaHandlerCallback.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2017 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.atlona.internal; + +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.atlona.handler.AtlonaHandler; + +/** + * + * A callback to {@link AtlonaHandler} that can be used to update the status, properties and state of the thing. + * + * @author Tim Roberts + * + */ +public interface AtlonaHandlerCallback { + /** + * Callback to the {@link AtlonaHandler} to update the status of the thing. + * + * @param status a non-null {@link org.eclipse.smarthome.core.thing.ThingStatus} + * @param detail a non-null {@link org.eclipse.smarthome.core.thing.ThingStatusDetail} + * @param msg a possibly null, possibly empty message + */ + void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg); + + /** + * Callback to the {@link AtlonaHandler} to update the state of an item + * + * @param channelId the non-null, non-empty channel id + * @param state the new non-null {@State} + */ + void stateChanged(String channelId, State state); + + /** + * Callback to the {@link AtlonaHandler} to update the property of a thing + * + * @param propertyName a non-null, non-empty property name + * @param propertyValue a non-null, possibly empty property value + */ + void setProperty(String propertyName, String propertyValue); +} diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/AtlonaHandlerFactory.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/AtlonaHandlerFactory.java new file mode 100644 index 0000000000000..127d21970db24 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/AtlonaHandlerFactory.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2017 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.atlona.internal; + +import java.util.Set; + +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.openhab.binding.atlona.AtlonaBindingConstants; +import org.openhab.binding.atlona.internal.pro3.AtlonaPro3Capabilities; +import org.openhab.binding.atlona.internal.pro3.AtlonaPro3Handler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableSet; + +/** + * The {@link org.openhab.binding.atlona.internal.AtlonaHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Tim Roberts + */ +public class AtlonaHandlerFactory extends BaseThingHandlerFactory { + + private Logger logger = LoggerFactory.getLogger(AtlonaHandlerFactory.class); + + /** + * The set of supported Atlona products + */ + private final static Set SUPPORTED_THING_TYPES_UIDS = ImmutableSet.of( + AtlonaBindingConstants.THING_TYPE_PRO3_44M, AtlonaBindingConstants.THING_TYPE_PRO3_66M, + AtlonaBindingConstants.THING_TYPE_PRO3_88M, AtlonaBindingConstants.THING_TYPE_PRO3_1616M); + + /** + * {@inheritDoc} + * + * Simply returns true if the given thingTypeUID is within {@link #SUPPORTED_THING_TYPES_UIDS} + */ + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + /** + * {@inheritDoc} + * + * Creates the handler for the given thing given its thingTypeUID + */ + @Override + protected ThingHandler createHandler(Thing thing) { + + if (thing == null) { + logger.error("createHandler was given a null thing!"); + return null; + } + + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (thingTypeUID.equals(AtlonaBindingConstants.THING_TYPE_PRO3_44M)) { + return new AtlonaPro3Handler(thing, new AtlonaPro3Capabilities(5, 3, ImmutableSet.of(5))); + } + + if (thingTypeUID.equals(AtlonaBindingConstants.THING_TYPE_PRO3_66M)) { + return new AtlonaPro3Handler(thing, new AtlonaPro3Capabilities(8, 4, ImmutableSet.of(6, 8))); + } + + if (thingTypeUID.equals(AtlonaBindingConstants.THING_TYPE_PRO3_88M)) { + return new AtlonaPro3Handler(thing, new AtlonaPro3Capabilities(10, 6, ImmutableSet.of(8, 10))); + } + + if (thingTypeUID.equals(AtlonaBindingConstants.THING_TYPE_PRO3_1616M)) { + return new AtlonaPro3Handler(thing, new AtlonaPro3Capabilities(5, 3, ImmutableSet.of(17, 18, 19, 20))); + } + + logger.warn("Unknown binding: {}: {}", thingTypeUID.getId(), thingTypeUID.getBindingId()); + return null; + } +} diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/StatefulHandlerCallback.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/StatefulHandlerCallback.java index 958682824bde8..f66075bf0dbc2 100644 --- a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/StatefulHandlerCallback.java +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/StatefulHandlerCallback.java @@ -132,4 +132,15 @@ public void setProperty(String propertyName, String propertyValue) { _wrappedCallback.setProperty(propertyName, propertyValue); } + + /** + * Callback to get the {@link State} for a given property name + * + * @param propertyName a possibly null, possibly empty property name + * @return the {@link State} for the propertyName or null if not found + */ + public State getState(String propertyName) { + // TODO Auto-generated method stub + return _state.get(propertyName); + } } diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/russound/internal/net/SocketChannelSession.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/net/SocketChannelSession.java similarity index 87% rename from addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/russound/internal/net/SocketChannelSession.java rename to addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/net/SocketChannelSession.java index f434d99605d94..58cabe0ad318c 100644 --- a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/russound/internal/net/SocketChannelSession.java +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/net/SocketChannelSession.java @@ -6,7 +6,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.russound.internal.net; +package org.openhab.binding.atlona.internal.net; import java.io.IOException; import java.net.InetSocketAddress; @@ -88,13 +88,6 @@ public SocketChannelSession(String host, int port) { _port = port; } - /* - * (non-Javadoc) - * - * @see - * org.openhab.binding.russound.internal.net.SocketSession#addListener(org.openhab.binding.russound.internal.net. - * SocketSessionListener) - */ @Override public void addListener(SocketSessionListener listener) { if (listener == null) { @@ -103,33 +96,16 @@ public void addListener(SocketSessionListener listener) { _listeners.add(listener); } - /* - * (non-Javadoc) - * - * @see org.openhab.binding.russound.internal.net.SocketSession#clearListeners() - */ @Override public void clearListeners() { _listeners.clear(); } - /* - * (non-Javadoc) - * - * @see - * org.openhab.binding.russound.internal.net.SocketSession#removeListener(org.openhab.binding.russound.internal.net. - * SocketSessionListener) - */ @Override public boolean removeListener(SocketSessionListener listener) { return _listeners.remove(listener); } - /* - * (non-Javadoc) - * - * @see org.openhab.binding.russound.internal.net.SocketSession#connect() - */ @Override public void connect() throws IOException { disconnect(); @@ -153,11 +129,6 @@ public void connect() throws IOException { new Thread(_responseReader).start(); } - /* - * (non-Javadoc) - * - * @see org.openhab.binding.russound.internal.net.SocketSession#disconnect() - */ @Override public void disconnect() throws IOException { if (isConnected()) { @@ -173,32 +144,18 @@ public void disconnect() throws IOException { } } - /* - * (non-Javadoc) - * - * @see org.openhab.binding.russound.internal.net.SocketSession#isConnected() - */ @Override public boolean isConnected() { final SocketChannel channel = _socketChannel.get(); return channel != null && channel.isConnected(); } - /* - * (non-Javadoc) - * - * @see org.openhab.binding.russound.internal.net.SocketSession#sendCommand(java.lang.String) - */ @Override public synchronized void sendCommand(String command) throws IOException { if (command == null) { throw new IllegalArgumentException("command cannot be null"); } - // if (command.trim().length() == 0) { - // throw new IllegalArgumentException("Command cannot be empty"); - // } - if (!isConnected()) { throw new IOException("Cannot send message - disconnected"); } @@ -234,8 +191,7 @@ private class ResponseReader implements Runnable { private final CountDownLatch _running = new CountDownLatch(1); /** - * Stops the reader. Will wait 5 seconds for the runnable to stop (should stop within 1 second based on the - * setSOTimeout) + * Stops the reader. Will wait 5 seconds for the runnable to stop */ public void stopRunning() { if (_isRunning.getAndSet(false)) { @@ -306,7 +262,7 @@ public void run() { } catch (InterruptedException e) { // Do nothing - probably shutting down } catch (AsynchronousCloseException e) { - // socket was definitelyclosed by another thread + // socket was definitely closed by another thread } catch (IOException e) { try { _isRunning.set(false); @@ -341,19 +297,28 @@ private class Dispatcher implements Runnable { */ private final CountDownLatch _running = new CountDownLatch(1); + /** + * Whether the dispatcher is currently processing a message + */ + private final AtomicReference _processingThread = new AtomicReference(); + /** * Stops the reader. Will wait 5 seconds for the runnable to stop (should stop within 1 second based on the poll * timeout below) */ public void stopRunning() { - if (_isRunning.getAndSet(false)) { - try { - if (!_running.await(5, TimeUnit.SECONDS)) { - _logger.warn("Waited too long for dispatcher to finish"); + // only wait if stopRunning didn't get called as part of processing a message + // (which would happen if we are processing an exception that forced a session close) + final Thread processingThread = _processingThread.get(); + if (processingThread != null && Thread.currentThread() != processingThread) { + try { + if (!_running.await(5, TimeUnit.SECONDS)) { + _logger.warn("Waited too long for dispatcher to finish"); + } + } catch (InterruptedException e) { + // do nothing } - } catch (InterruptedException e) { - // do nothing } } } @@ -363,13 +328,14 @@ public void stopRunning() { */ @Override public void run() { + _processingThread.set(Thread.currentThread()); + _isRunning.set(true); while (_isRunning.get()) { try { - final SocketSessionListener[] listeners = _listeners.toArray(new SocketSessionListener[0]); // if no listeners, we don't want to start dispatching yet. - if (listeners.length == 0) { + if (_listeners.size() == 0) { Thread.sleep(250); continue; } @@ -380,6 +346,8 @@ public void run() { if (response instanceof String) { try { _logger.debug("Dispatching response: {}", response); + final SocketSessionListener[] listeners = _listeners + .toArray(new SocketSessionListener[0]); for (SocketSessionListener listener : listeners) { listener.responseReceived((String) response); } @@ -388,6 +356,7 @@ public void run() { } } else if (response instanceof Exception) { _logger.debug("Dispatching exception: {}", response); + final SocketSessionListener[] listeners = _listeners.toArray(new SocketSessionListener[0]); for (SocketSessionListener listener : listeners) { listener.responseException((Exception) response); } @@ -397,10 +366,13 @@ public void run() { } } catch (InterruptedException e) { // Do nothing + } catch (Exception e) { + _logger.debug("Uncaught exception {}", e.getMessage(), e); + break; } } _isRunning.set(false); - + _processingThread.set(null); _running.countDown(); } } diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/russound/internal/net/SocketSession.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/net/SocketSession.java similarity index 98% rename from addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/russound/internal/net/SocketSession.java rename to addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/net/SocketSession.java index b8ba9ffd12748..ebb55f5e7fb40 100644 --- a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/russound/internal/net/SocketSession.java +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/net/SocketSession.java @@ -6,7 +6,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.russound.internal.net; +package org.openhab.binding.atlona.internal.net; import java.io.IOException; diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/russound/internal/net/SocketSessionListener.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/net/SocketSessionListener.java similarity index 94% rename from addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/russound/internal/net/SocketSessionListener.java rename to addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/net/SocketSessionListener.java index 89774a93cec68..348b82c586c98 100644 --- a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/russound/internal/net/SocketSessionListener.java +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/net/SocketSessionListener.java @@ -6,7 +6,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.russound.internal.net; +package org.openhab.binding.atlona.internal.net; /** * Interface defining a listener to a {@link SocketSession} that will receive responses and/or exceptions from the diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Capabilities.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Capabilities.java new file mode 100644 index 0000000000000..ce12a6845a662 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Capabilities.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2017 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.atlona.internal.pro3; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.openhab.binding.atlona.handler.AtlonaCapabilities; + +/** + * The capabilities class for the Atlona PRO3 line. Each PRO3 model differs in the number of (output) ports that can be + * powered, the number of audio ports there are and which (output) ports are HDMI ports. + * + * @author Tim Roberts + */ +public class AtlonaPro3Capabilities extends AtlonaCapabilities { + + /** + * Number of power ports + */ + private final int _nbrPowerPorts; + + /** + * Number of audio ports + */ + private final int _nbrAudioPorts; + + /** + * The set of output ports that are HDMI ports + */ + private final Set _hdmiPorts; + + /** + * Constructs the capabilities from the parms + * + * @param nbrPowerPorts a greater than 0 number of power ports + * @param nbrAudioPorts a greater than 0 number of audio ports + * @param hdmiPorts a non-null, non-empty set of hdmi ports + */ + public AtlonaPro3Capabilities(int nbrPowerPorts, int nbrAudioPorts, Set hdmiPorts) { + super(); + + if (nbrPowerPorts < 1) { + throw new IllegalArgumentException("nbrPowerPorts must be greater than 0"); + } + + if (nbrAudioPorts < 1) { + throw new IllegalArgumentException("nbrAudioPorts must be greater than 0"); + } + + if (hdmiPorts == null) { + throw new IllegalArgumentException("hdmiPorts cannot be null"); + } + + if (hdmiPorts.size() == 0) { + throw new IllegalArgumentException("hdmiPorts cannot be empty"); + } + + _nbrPowerPorts = nbrPowerPorts; + _nbrAudioPorts = nbrAudioPorts; + _hdmiPorts = Collections.unmodifiableSet(new HashSet(hdmiPorts)); + } + + /** + * Returns the number of power ports + * + * @return a greater than 0 number of power ports + */ + int getNbrPowerPorts() { + return _nbrPowerPorts; + } + + /** + * Returns the number of audio ports + * + * @return a greater than 0 number of audio ports + */ + int getNbrAudioPorts() { + return _nbrAudioPorts; + } + + /** + * Returns the set of hdmi ports + * + * @return a non-null, non-empty immutable set of hdmi ports + */ + Set getHdmiPorts() { + return _hdmiPorts; + } +} diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Config.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Config.java new file mode 100644 index 0000000000000..bb00dedf357b1 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Config.java @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2010-2017 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.atlona.internal.pro3; + +import org.openhab.binding.atlona.discovery.AtlonaDiscovery; + +/** + * Configuration class for the Atlona Pro3 line of switchers + * + * @author Tim Roberts + */ +public class AtlonaPro3Config { + + /** + * Constant field used in {@link AtlonaDiscovery} to set the config property during discovery. Value of this field + * needs to match {@link #ipAddress} + */ + public final static String IpAddress = "ipAddress"; + + /** + * IP Address (or host name) of switch + */ + private String ipAddress; + + /** + * Optional username to login in with. Only used if the switch has it's "Telnet Login" option turned on + */ + private String userName; + + /** + * Optional password to login in with. Only used if the switch has it's "Telnet Login" option turned on + */ + private String password; + + /** + * Polling time (in seconds) to refresh state from the switch itself. Only useful if something else modifies the + * switch (usually through the front panel or the IR link) + */ + private int polling; + + /** + * Ping time (in seconds) to keep the connection alive. Should be less than the IP Timeout on the switch. + */ + private int ping; + + /** + * Polling time (in seconds) to attempt a reconnect if the socket session has failed + */ + private int retryPolling; + + /** + * Returns the IP address or host name of the switch + * + * @return the IP address or host name of the swtich + */ + public String getIpAddress() { + return ipAddress; + } + + /** + * Sets the IP address or host name of the switch + * + * @param ipAddress the IP Address or host name of the switch + */ + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + /** + * Gets the username used to login with + * + * @return the username used to login with + */ + public String getUserName() { + return userName; + } + + /** + * Sets the username used to login with + * + * @param userName the username used to login with + */ + public void setUserName(String userName) { + this.userName = userName; + } + + /** + * Gets the password used to login with + * + * @return the password used to login with + */ + public String getPassword() { + return password; + } + + /** + * Sets the password used to login with + * + * @param password the password used to login with + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Gets the polling (in seconds) to refresh state + * + * @return the polling (in seconds) to refresh state + */ + public int getPolling() { + return polling; + } + + /** + * Sets the polling (in seconds) to refresh state + * + * @param polling the polling (in seconds) to refresh state + */ + public void setPolling(int polling) { + this.polling = polling; + } + + /** + * Gets the polling (in seconds) to reconnect + * + * @return the polling (in seconds) to reconnect + */ + public int getRetryPolling() { + return retryPolling; + } + + /** + * Sets the polling (in seconds) to reconnect + * + * @param retryPolling the polling (in seconds to reconnect) + */ + public void setRetryPolling(int retryPolling) { + this.retryPolling = retryPolling; + } + + /** + * Gets the ping interval (in seconds) + * + * @return the ping interval (in seconds) + */ + public int getPing() { + return ping; + } + + /** + * Sets the ping interval (in seconds) + * + * @param ping the ping interval (in seconds) + */ + public void setPing(int ping) { + this.ping = ping; + } +} diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Constants.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Constants.java new file mode 100644 index 0000000000000..b24cad1f64f87 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Constants.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2017 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.atlona.internal.pro3; + +/** + * The {@link AtlonaPro3Binding} class defines common constants, which are + * used across the whole binding. + * + * @author Tim Roberts + */ +class AtlonaPro3Constants { + + // Properties + final static String PROPERTY_VERSION = "version"; + final static String PROPERTY_TYPE = "type"; + + final static String GROUP_PRIMARY = "primary"; + final static String GROUP_PORT = "port"; + final static String GROUP_MIRROR = "mirror"; + final static String GROUP_VOLUME = "volume"; + + // List of all Channel ids + final static String CHANNEL_POWER = "power"; + final static String CHANNEL_PANELLOCK = "panellock"; + final static String CHANNEL_IRENABLE = "irenable"; + final static String CHANNEL_PRESETCMDS = "presetcmd"; + final static String CHANNEL_MATRIXCMDS = "matrixcmd"; + + final static String CHANNEL_PORTPOWER = "portpower"; + final static String CHANNEL_PORTOUTPUT = "portoutput"; + + final static String CHANNEL_PORTMIRROR = "portmirror"; + final static String CHANNEL_PORTMIRRORENABLED = "portmirrorenabled"; + + final static String CHANNEL_VOLUME = "volume"; + final static String CHANNEL_VOLUME_MUTE = "volumemute"; + // final static String CHANNEL_RS232 = "rs232cmd"; + + final static String CONFIG_HOSTNAME = "hostname"; + final static String CONFIG_OUTPUT = "output"; + + // Preset commands + final static String CMD_PRESETSAVE = "save"; + final static String CMD_PRESETRECALL = "recall"; + final static String CMD_PRESETCLEAR = "clear"; + + // Matrix commands + final static String CMD_MATRIXRESET = "resetmatrix"; + final static String CMD_MATRIXRESETPORTS = "resetports"; + final static String CMD_MATRIXPORTALL = "allports"; + +} diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Handler.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Handler.java new file mode 100644 index 0000000000000..630ecb75a619c --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Handler.java @@ -0,0 +1,630 @@ +/** + * Copyright (c) 2010-2017 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.atlona.internal.pro3; + +import java.io.IOException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +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.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.atlona.handler.AtlonaHandler; +import org.openhab.binding.atlona.internal.AtlonaHandlerCallback; +import org.openhab.binding.atlona.internal.StatefulHandlerCallback; +import org.openhab.binding.atlona.internal.net.SocketChannelSession; +import org.openhab.binding.atlona.internal.net.SocketSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link org.openhab.binding.atlona.internal.pro3.AtlonaPro3Handler} is responsible for handling commands, which + * are + * sent to one of the channels. + * + * @author Tim Roberts + */ +public class AtlonaPro3Handler extends AtlonaHandler { + + private Logger logger = LoggerFactory.getLogger(AtlonaPro3Handler.class); + + /** + * The {@link AtlonaPro3PortocolHandler} protocol handler + */ + private AtlonaPro3PortocolHandler _atlonaHandler; + + /** + * The {@link SocketSession} telnet session to the switch. Will be null if not connected. + */ + private SocketSession _session; + + /** + * The polling job to poll the actual state from the {@link #_session} + */ + private ScheduledFuture _polling; + + /** + * The retry connection event + */ + private ScheduledFuture _retryConnection; + + /** + * The ping event + */ + private ScheduledFuture _ping; + + // List of all the groups patterns we recognize + private final static Pattern GROUP_PRIMARY_PATTERN = Pattern.compile("^" + AtlonaPro3Constants.GROUP_PRIMARY + "$"); + private final static Pattern GROUP_PORT_PATTERN = Pattern + .compile("^" + AtlonaPro3Constants.GROUP_PORT + "(\\d{1,2})$"); + private final static Pattern GROUP_MIRROR_PATTERN = Pattern + .compile("^" + AtlonaPro3Constants.GROUP_MIRROR + "(\\d{1,2})$"); + private final static Pattern GROUP_VOLUME_PATTERN = Pattern + .compile("^" + AtlonaPro3Constants.GROUP_VOLUME + "(\\d{1,2})$"); + + // List of preset commands we recognize + private final static Pattern CMD_PRESETSAVE = Pattern + .compile("^" + AtlonaPro3Constants.CMD_PRESETSAVE + "(\\d{1,2})$"); + private final static Pattern CMD_PRESETRECALL = Pattern + .compile("^" + AtlonaPro3Constants.CMD_PRESETRECALL + "(\\d{1,2})$"); + private final static Pattern CMD_PRESETCLEAR = Pattern + .compile("^" + AtlonaPro3Constants.CMD_PRESETCLEAR + "(\\d{1,2})$"); + + // List of matrix commands we recognize + private final static Pattern CMD_MATRIXRESET = Pattern.compile("^" + AtlonaPro3Constants.CMD_MATRIXRESET + "$"); + private final static Pattern CMD_MATRIXRESETPORTS = Pattern + .compile("^" + AtlonaPro3Constants.CMD_MATRIXRESETPORTS + "$"); + private final static Pattern CMD_MATRIXPORTALL = Pattern + .compile("^" + AtlonaPro3Constants.CMD_MATRIXPORTALL + "(\\d{1,2})$"); + + /** + * Constructs the handler from the {@link org.eclipse.smarthome.core.thing.Thing} with the number of power ports and + * audio ports the switch supports. + * + * @param thing a non-null {@link org.eclipse.smarthome.core.thing.Thing} the handler is for + * @param capabilities a non-null {@link org.openhab.binding.atlona.internal.pro3.AtlonaPro3Capabilities} + */ + public AtlonaPro3Handler(Thing thing, AtlonaPro3Capabilities capabilities) { + super(thing, capabilities); + + if (thing == null) { + throw new IllegalArgumentException("thing cannot be null"); + } + } + + /** + * {@inheritDoc} + * + * Handles commands to specific channels. This implementation will offload much of its work to the + * {@link AtlonaPro3PortocolHandler}. Basically we validate the type of command for the channel then call the + * {@link AtlonaPro3PortocolHandler} to handle the actual protocol. Special use case is the {@link RefreshType} + * where we call {{@link #handleRefresh(String)} to handle a refresh of the specific channel (which in turn calls + * {@link AtlonaPro3PortocolHandler} to handle the actual refresh + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + + if (command instanceof RefreshType) { + handleRefresh(channelUID); + return; + } + + final String group = channelUID.getGroupId().toLowerCase(); + final String id = channelUID.getIdWithoutGroup().toLowerCase(); + + Matcher m; + if ((m = GROUP_PRIMARY_PATTERN.matcher(group)).matches()) { + switch (id) { + case AtlonaPro3Constants.CHANNEL_POWER: + if (command instanceof OnOffType) { + final boolean makeOn = ((OnOffType) command) == OnOffType.ON; + _atlonaHandler.setPower(makeOn); + } else { + logger.debug("Received a POWER channel command with a non OnOffType: {}", command); + } + + break; + + case AtlonaPro3Constants.CHANNEL_PANELLOCK: + if (command instanceof OnOffType) { + final boolean makeOn = ((OnOffType) command) == OnOffType.ON; + _atlonaHandler.setPanelLock(makeOn); + } else { + logger.debug("Received a PANELLOCK channel command with a non OnOffType: {}", command); + } + break; + + case AtlonaPro3Constants.CHANNEL_IRENABLE: + if (command instanceof OnOffType) { + final boolean makeOn = ((OnOffType) command) == OnOffType.ON; + _atlonaHandler.setIrOn(makeOn); + } else { + logger.debug("Received a IRLOCK channel command with a non OnOffType: {}", command); + } + + break; + case AtlonaPro3Constants.CHANNEL_MATRIXCMDS: + if (command instanceof StringType) { + final String matrixCmd = command.toString(); + Matcher cmd; + try { + if ((cmd = CMD_MATRIXRESET.matcher(matrixCmd)).matches()) { + _atlonaHandler.resetMatrix(); + } else if ((cmd = CMD_MATRIXRESETPORTS.matcher(matrixCmd)).matches()) { + _atlonaHandler.resetAllPorts(); + } else if ((cmd = CMD_MATRIXPORTALL.matcher(matrixCmd)).matches()) { + if (cmd.groupCount() == 1) { + final int portNbr = Integer.parseInt(cmd.group(1)); + _atlonaHandler.setPortAll(portNbr); + } else { + logger.debug("Unknown matirx set port command: '{}'", matrixCmd); + } + + } else { + logger.debug("Unknown matrix command: '{}'", cmd); + } + } catch (NumberFormatException e) { + logger.debug("Could not parse the port number from the command: '{}'", matrixCmd); + } + } + break; + case AtlonaPro3Constants.CHANNEL_PRESETCMDS: + if (command instanceof StringType) { + final String presetCmd = command.toString(); + Matcher cmd; + try { + if ((cmd = CMD_PRESETSAVE.matcher(presetCmd)).matches()) { + if (cmd.groupCount() == 1) { + final int presetNbr = Integer.parseInt(cmd.group(1)); + _atlonaHandler.saveIoSettings(presetNbr); + } else { + logger.debug("Unknown preset save command: '{}'", presetCmd); + } + } else if ((cmd = CMD_PRESETRECALL.matcher(presetCmd)).matches()) { + if (cmd.groupCount() == 1) { + final int presetNbr = Integer.parseInt(cmd.group(1)); + _atlonaHandler.recallIoSettings(presetNbr); + } else { + logger.debug("Unknown preset recall command: '{}'", presetCmd); + } + } else if ((cmd = CMD_PRESETCLEAR.matcher(presetCmd)).matches()) { + if (cmd.groupCount() == 1) { + final int presetNbr = Integer.parseInt(cmd.group(1)); + _atlonaHandler.clearIoSettings(presetNbr); + } else { + logger.debug("Unknown preset clear command: '{}'", presetCmd); + } + + } else { + logger.debug("Unknown preset command: '{}'", cmd); + } + } catch (NumberFormatException e) { + logger.debug("Could not parse the preset number from the command: '{}'", presetCmd); + } + } + break; + + default: + logger.debug("Unknown/Unsupported Primary Channel: {}", channelUID.getAsString()); + break; + } + + } else if ((m = GROUP_PORT_PATTERN.matcher(group)).matches()) { + if (m.groupCount() == 1) { + try { + final int portNbr = Integer.parseInt(m.group(1)); + + switch (id) { + case AtlonaPro3Constants.CHANNEL_PORTOUTPUT: + if (command instanceof DecimalType) { + final int inpNbr = ((DecimalType) command).intValue(); + _atlonaHandler.setPortSwitch(inpNbr, portNbr); + } else { + logger.debug("Received a PORTOUTPUT channel command with a non DecimalType: {}", + command); + } + + break; + + case AtlonaPro3Constants.CHANNEL_PORTPOWER: + if (command instanceof OnOffType) { + final boolean makeOn = ((OnOffType) command) == OnOffType.ON; + _atlonaHandler.setPortPower(portNbr, makeOn); + } else { + logger.debug("Received a PORTPOWER channel command with a non OnOffType: {}", command); + } + break; + default: + logger.debug("Unknown/Unsupported Port Channel: {}", channelUID.getAsString()); + break; + } + } catch (NumberFormatException e) { + logger.debug("Bad Port Channel (can't parse the port nbr): {}", channelUID.getAsString()); + } + } + } else if ((m = GROUP_MIRROR_PATTERN.matcher(group)).matches()) { + if (m.groupCount() == 1) { + try { + final int hdmiPortNbr = Integer.parseInt(m.group(1)); + + switch (id) { + case AtlonaPro3Constants.CHANNEL_PORTMIRROR: + if (command instanceof DecimalType) { + final int outPortNbr = ((DecimalType) command).intValue(); + if (outPortNbr <= 0) { + _atlonaHandler.removePortMirror(hdmiPortNbr); + } else { + _atlonaHandler.setPortMirror(hdmiPortNbr, outPortNbr); + } + } else { + logger.debug("Received a PORTMIRROR channel command with a non DecimalType: {}", + command); + } + + break; + case AtlonaPro3Constants.CHANNEL_PORTMIRRORENABLED: + if (command instanceof OnOffType) { + if (command == OnOffType.ON) { + final StatefulHandlerCallback callback = (StatefulHandlerCallback) _atlonaHandler + .getCallback(); + final State state = callback.getState(AtlonaPro3Constants.CHANNEL_PORTMIRROR); + int outPortNbr = 1; + if (state != null && state instanceof DecimalType) { + outPortNbr = ((DecimalType) state).intValue(); + } + _atlonaHandler.setPortMirror(hdmiPortNbr, outPortNbr); + } else { + _atlonaHandler.removePortMirror(hdmiPortNbr); + } + } else { + logger.debug("Received a PORTMIRROR channel command with a non DecimalType: {}", + command); + } + + break; + default: + logger.debug("Unknown/Unsupported Mirror Channel: {}", channelUID.getAsString()); + break; + } + } catch (NumberFormatException e) { + logger.debug("Bad Mirror Channel (can't parse the port nbr): {}", channelUID.getAsString()); + } + } + } else if ((m = GROUP_VOLUME_PATTERN.matcher(group)).matches()) { + if (m.groupCount() == 1) { + try { + final int portNbr = Integer.parseInt(m.group(1)); + + switch (id) { + case AtlonaPro3Constants.CHANNEL_VOLUME_MUTE: + if (command instanceof OnOffType) { + _atlonaHandler.setVolumeMute(portNbr, ((OnOffType) command) == OnOffType.ON); + } else { + logger.debug("Received a VOLUME MUTE channel command with a non OnOffType: {}", + command); + } + + break; + case AtlonaPro3Constants.CHANNEL_VOLUME: + if (command instanceof DecimalType) { + final double level = ((DecimalType) command).doubleValue(); + _atlonaHandler.setVolume(portNbr, level); + } else { + logger.debug("Received a VOLUME channel command with a non DecimalType: {}", command); + } + break; + + default: + logger.debug("Unknown/Unsupported Volume Channel: {}", channelUID.getAsString()); + break; + } + } catch (NumberFormatException e) { + logger.debug("Bad Volume Channel (can't parse the port nbr): {}", channelUID.getAsString()); + } + } + } else { + logger.debug("Unknown/Unsupported Channel: {}", channelUID.getAsString()); + } + } + + /** + * Method that handles the {@link RefreshType} command specifically. Calls the {@link AtlonaPro3PortocolHandler} to + * handle the actual refresh based on the channel id. + * + * @param id a non-null, possibly empty channel id to refresh + */ + private void handleRefresh(ChannelUID channelUID) { + if (getThing().getStatus() != ThingStatus.ONLINE) { + return; + } + + final String group = channelUID.getGroupId().toLowerCase(); + final String id = channelUID.getIdWithoutGroup().toLowerCase(); + final StatefulHandlerCallback callback = (StatefulHandlerCallback) _atlonaHandler.getCallback(); + + Matcher m; + if ((m = GROUP_PRIMARY_PATTERN.matcher(group)).matches()) { + switch (id) { + case AtlonaPro3Constants.CHANNEL_POWER: + callback.removeState(AtlonaPro3Utilities.createChannelID(group, id)); + _atlonaHandler.refreshPower(); + break; + + default: + break; + } + + } else if ((m = GROUP_PORT_PATTERN.matcher(group)).matches()) { + if (m.groupCount() == 1) { + try { + final int portNbr = Integer.parseInt(m.group(1)); + callback.removeState(AtlonaPro3Utilities.createChannelID(group, portNbr, id)); + + switch (id) { + case AtlonaPro3Constants.CHANNEL_PORTOUTPUT: + _atlonaHandler.refreshPortStatus(portNbr); + break; + + case AtlonaPro3Constants.CHANNEL_PORTPOWER: + _atlonaHandler.refreshPortPower(portNbr); + break; + default: + break; + } + } catch (NumberFormatException e) { + logger.debug("Bad Port Channel (can't parse the port nbr): {}", channelUID.getAsString()); + } + + } + } else if ((m = GROUP_MIRROR_PATTERN.matcher(group)).matches()) { + if (m.groupCount() == 1) { + try { + final int hdmiPortNbr = Integer.parseInt(m.group(1)); + callback.removeState(AtlonaPro3Utilities.createChannelID(group, hdmiPortNbr, id)); + _atlonaHandler.refreshPortMirror(hdmiPortNbr); + } catch (NumberFormatException e) { + logger.debug("Bad Mirror Channel (can't parse the port nbr): {}", channelUID.getAsString()); + } + + } + } else if ((m = GROUP_VOLUME_PATTERN.matcher(group)).matches()) { + if (m.groupCount() == 1) { + try { + final int portNbr = Integer.parseInt(m.group(1)); + callback.removeState(AtlonaPro3Utilities.createChannelID(group, portNbr, id)); + + switch (id) { + case AtlonaPro3Constants.CHANNEL_VOLUME_MUTE: + _atlonaHandler.refreshVolumeMute(portNbr); + break; + case AtlonaPro3Constants.CHANNEL_VOLUME: + _atlonaHandler.refreshVolumeStatus(portNbr); + break; + + default: + break; + } + } catch (NumberFormatException e) { + logger.debug("Bad Volume Channel (can't parse the port nbr): {}", channelUID.getAsString()); + } + + } + } else { + // nothing else matters... + } + } + + /** + * {@inheritDoc} + * + * Initializes the handler. This initialization will read/validate the configuration, then will create the + * {@link SocketSession}, initialize the {@link AtlonaPro3PortocolHandler} and will attempt to connect to the switch + * (via {{@link #retryConnect()}. + */ + @Override + public void initialize() { + final AtlonaPro3Config config = getAtlonaConfig(); + + if (config == null) { + return; + } + + if (config.getIpAddress() == null || config.getIpAddress().trim().length() == 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "IP Address of Atlona Pro3 is missing from configuration"); + return; + } + + _session = new SocketChannelSession(config.getIpAddress(), 23); + _atlonaHandler = new AtlonaPro3PortocolHandler(_session, config, getCapabilities(), + new StatefulHandlerCallback(new AtlonaHandlerCallback() { + @Override + public void stateChanged(String channelId, State state) { + updateState(channelId, state); + } + + @Override + public void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg) { + updateStatus(status, detail, msg); + + if (status != ThingStatus.ONLINE) { + disconnect(true); + } + } + + @Override + public void setProperty(String propertyName, String propertyValue) { + getThing().setProperty(propertyName, propertyValue); + } + })); + + // Try initial connection in a scheduled task + this.scheduler.schedule(new Runnable() { + @Override + public void run() { + connect(); + } + + }, 1, TimeUnit.SECONDS); + } + + /** + * Attempts to connect to the switch. If successfully connect, the {@link AtlonaPro3PortocolHandler#login()} will be + * called to log into the switch (if needed). Once completed, a polling job will be created to poll the switch's + * actual state and a ping job to ping the server. If a connection cannot be established (or login failed), the + * connection attempt will be retried later (via {@link #retryConnect()}) + */ + private void connect() { + String response = "Server is offline - will try to reconnect later"; + try { + // clear listeners to avoid any 'old' listener from handling initial messages + _session.clearListeners(); + _session.connect(); + + response = _atlonaHandler.login(); + if (response == null) { + final AtlonaPro3Config config = getAtlonaConfig(); + if (config != null) { + + _polling = this.scheduler.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + final ThingStatus status = getThing().getStatus(); + if (status == ThingStatus.ONLINE) { + if (_session.isConnected()) { + _atlonaHandler.refreshAll(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Atlona PRO3 has disconnected. Will try to reconnect later."); + + } + } else if (status == ThingStatus.OFFLINE) { + disconnect(true); + } + + } + }, config.getPolling(), config.getPolling(), TimeUnit.SECONDS); + + _ping = this.scheduler.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + final ThingStatus status = getThing().getStatus(); + if (status == ThingStatus.ONLINE) { + if (_session.isConnected()) { + _atlonaHandler.ping(); + } + } + + } + }, config.getPing(), config.getPing(), TimeUnit.SECONDS); + + updateStatus(ThingStatus.ONLINE); + return; + } + } + + } catch (Exception e) { + // do nothing + } + + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, response); + retryConnect(); + } + + /** + * Attempts to disconnect from the session and will optionally retry the connection attempt. The {@link #_polling} + * will be cancelled, the {@link #_ping} will be cancelled and both set to null then the {@link #_session} will be + * disconnected. + * + * @param retryConnection true to retry connection attempts after the disconnect + */ + private void disconnect(boolean retryConnection) { + // Cancel polling + if (_polling != null) { + _polling.cancel(true); + _polling = null; + } + + // Cancel ping + if (_ping != null) { + _ping.cancel(true); + _ping = null; + } + + try { + _session.disconnect(); + } catch (IOException e) { + // ignore - we don't care + } + + if (retryConnection) { + retryConnect(); + } + } + + /** + * Retries the connection attempt - schedules a job in {@link AtlonaPro3Config#getRetryPolling()} seconds to call + * the + * {@link #connect()} method. If a retry attempt is pending, the request is ignored. + */ + private void retryConnect() { + if (_retryConnection == null) { + final AtlonaPro3Config config = getAtlonaConfig(); + if (config != null) { + + logger.info("Will try to reconnect in {} seconds", config.getRetryPolling()); + _retryConnection = this.scheduler.schedule(new Runnable() { + @Override + public void run() { + _retryConnection = null; + connect(); + } + + }, config.getRetryPolling(), TimeUnit.SECONDS); + } + } else { + logger.debug("RetryConnection called when a retry connection is pending - ignoring request"); + } + } + + /** + * Simple gets the {@link AtlonaPro3Config} from the {@link Thing} and will set the status to offline if not found. + * + * @return a possible null {@link AtlonaPro3Config} + */ + private AtlonaPro3Config getAtlonaConfig() { + final AtlonaPro3Config config = getThing().getConfiguration().as(AtlonaPro3Config.class); + + if (config == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR); + } + + return config; + } + + /** + * {@inheritDoc} + * + * Disposes of the handler. Will simply call {@link #disconnect(boolean)} to disconnect and NOT retry the + * connection + */ + @Override + public void dispose() { + disconnect(false); + } +} diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3PortocolHandler.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3PortocolHandler.java new file mode 100644 index 0000000000000..08d9828597234 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3PortocolHandler.java @@ -0,0 +1,1168 @@ +/** + * Copyright (c) 2010-2017 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.atlona.internal.pro3; + +import java.io.IOException; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang.StringUtils; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.openhab.binding.atlona.internal.AtlonaHandlerCallback; +import org.openhab.binding.atlona.internal.net.SocketSession; +import org.openhab.binding.atlona.internal.net.SocketSessionListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the protocol handler for the PRO3 product line. This handler will issue the protocol commands and will + * process the responses from the PRO3 switch. This handler was written to respond to any response that can be sent from + * the TCP/IP session (either in response to our own commands or in response to external events [other TCP/IP sessions, + * web GUI, front panel keystrokes, etc]). + * + * @author Tim Roberts + * + */ +class AtlonaPro3PortocolHandler { + private Logger logger = LoggerFactory.getLogger(AtlonaPro3PortocolHandler.class); + + /** + * The {@link SocketSession} used by this protocol handler + */ + private final SocketSession _session; + + /** + * The {@link AtlonaPro3Config} configuration used by this handler + */ + private final AtlonaPro3Config _config; + + /** + * The {@link AtlonaPro3Capabilities} of the PRO3 model + */ + private final AtlonaPro3Capabilities _capabilities; + + /** + * The {@link AtlonaPro3Handler} to call back to update status and state + */ + private final AtlonaHandlerCallback _callback; + + /** + * The model type identified by the switch. We save it for faster refreshes since it will not change + */ + private String _modelType; + + /** + * The version (firmware) identified by the switch. We save it for faster refreshes since it will not change between + * sessions + */ + private String _version; + + /** + * A special (invalid) command used internally by this handler to identify whether the switch wants a login or not + * (see {@link #login()}) + */ + private final static String NOTVALID_USER_OR_CMD = "notvalid$934%912"; + + // ------------------------------------------------------------------------------------------------ + // The following are the various command formats specified by the Atlona protocol + private final static String CMD_POWERON = "PWON"; + private final static String CMD_POWEROFF = "PWOFF"; + private final static String CMD_POWER_STATUS = "PWSTA"; + private final static String CMD_VERSION = "Version"; + private final static String CMD_TYPE = "Type"; + private final static String CMD_PANELLOCK = "Lock"; + private final static String CMD_PANELUNLOCK = "Unlock"; + private final static String CMD_PORT_RESETALL = "All#"; + private final static String CMD_PORT_POWER_FORMAT = "x%d$ %s"; + private final static String CMD_PORT_ALL_FORMAT = "x%dAll"; + private final static String CMD_PORT_SWITCH_FORMAT = "x%dAVx%d"; + private final static String CMD_PORT_MIRROR_FORMAT = "MirrorHdmi%d Out%d"; + private final static String CMD_PORT_MIRROR_STATUS_FORMAT = "MirrorHdmi%d sta"; + private final static String CMD_PORT_UNMIRROR_FORMAT = "UnMirror%d"; + private final static String CMD_VOLUME_FORMAT = "VOUT%d %s"; + private final static String CMD_VOLUME_MUTE_FORMAT = "VOUTMute%d %s"; + private final static String CMD_IROFF = "IROFF"; + private final static String CMD_IRON = "IRON"; + private final static String CMD_PORT_STATUS = "Status"; + private final static String CMD_PORT_STATUS_FORMAT = "Statusx%d"; + private final static String CMD_SAVEIO_FORMAT = "Save%d"; + private final static String CMD_RECALLIO_FORMAT = "Recall%d"; + private final static String CMD_CLEARIO_FORMAT = "Clear%d"; + private final static String CMD_MATRIX_RESET = "Mreset"; + private final static String CMD_BROADCAST_ON = "Broadcast on"; + + // ------------------------------------------------------------------------------------------------ + // The following are the various responses specified by the Atlona protocol + private final static String RSP_FAILED = "Command FAILED:"; + + private final static String RSP_LOGIN = "Login"; + private final static String RSP_PASSWORD = "Password"; + + private final Pattern _powerStatusPattern = Pattern.compile("PW(\\w+)"); + private final Pattern _versionPattern = Pattern.compile("Firmware (.*)"); + private final Pattern _typePattern = Pattern.compile("AT-UHD-PRO3-(\\d+)M"); + private final static String RSP_ALL = "All#"; + private final static String RSP_LOCK = "Lock"; + private final static String RSP_UNLOCK = "Unlock"; + private final Pattern _portStatusPattern = Pattern.compile("x(\\d+)AVx(\\d+),?+"); + private final Pattern _portPowerPattern = Pattern.compile("x(\\d+)\\$ (\\w+)"); + private final Pattern _portAllPattern = Pattern.compile("x(\\d+)All"); + private final Pattern _portMirrorPattern = Pattern.compile("MirrorHdmi(\\d+) (\\p{Alpha}+)(\\d*)"); + private final Pattern _portUnmirrorPattern = Pattern.compile("UnMirror(\\d+)"); + private final Pattern _volumePattern = Pattern.compile("VOUT(\\d+) (-?\\d+)"); + private final Pattern _volumeMutePattern = Pattern.compile("VOUTMute(\\d+) (\\w+)"); + private final static String RSP_IROFF = "IROFF"; + private final static String RSP_IRON = "IRON"; + private final Pattern _saveIoPattern = Pattern.compile("Save(\\d+)"); + private final Pattern _recallIoPattern = Pattern.compile("Recall(\\d+)"); + private final Pattern _clearIoPattern = Pattern.compile("Clear(\\d+)"); + private final Pattern _broadCastPattern = Pattern.compile("Broadcast (\\w+)"); + private final static String RSP_MATRIX_RESET = "Mreset"; + + // ------------------------------------------------------------------------------------------------ + // The following isn't part of the atlona protocol and is generated by us + private final static String CMD_PING = "ping"; + private final static String RSP_PING = "Command FAILED: (ping)"; + + /** + * Constructs the protocol handler from given parameters + * + * @param session a non-null {@link SocketSession} (may be connected or disconnected) + * @param config a non-null {@link AtlonaPro3Config} + * @param capabilities a non-null {@link AtlonaPro3Capabilities} + * @param callback a non-null {@link AtlonaHandlerCallback} to update state and status + */ + AtlonaPro3PortocolHandler(SocketSession session, AtlonaPro3Config config, AtlonaPro3Capabilities capabilities, + AtlonaHandlerCallback callback) { + + if (session == null) { + throw new IllegalArgumentException("session cannot be null"); + } + + if (config == null) { + throw new IllegalArgumentException("config cannot be null"); + } + + if (capabilities == null) { + throw new IllegalArgumentException("capabilities cannot be null"); + } + + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + + _session = session; + _config = config; + _capabilities = capabilities; + _callback = callback; + } + + /** + * Attempts to log into the switch when prompted by the switch. Please see code comments on the exact protocol for + * this. + * + * @return a null if logged in successfully (or if switch didn't require login). Non-null if an exception occurred. + * @throws IOException an IO exception occurred during login + */ + String login() throws Exception { + + logger.debug("Logging into atlona switch"); + // Void to make sure we retrieve them + _modelType = null; + _version = null; + + NoDispatchingCallback callback = new NoDispatchingCallback(); + _session.addListener(callback); + + // Burn the initial (empty) return + String response; + try { + response = callback.getResponse(); + if (!response.equals("")) { + logger.info("Altona protocol violation - didn't start with an inital empty response: '{}'", response); + } + } catch (Exception e) { + // ignore - may not having given us an initial "" + } + + // At this point - we are not sure if it's: + // 1) waiting for a command input + // or 2) has sent a "Login: " prompt + // By sending a string that doesn't exist as a command or user + // we can tell which by the response to the invalid command + _session.sendCommand(NOTVALID_USER_OR_CMD); + + // Command failed - Altona not configured with IPLogin - return success + response = callback.getResponse(); + if (response.startsWith(RSP_FAILED)) { + logger.debug("Altona didn't require a login"); + postLogin(); + return null; + } + + // We should have been presented wit a new "\r\nLogin: " + response = callback.getResponse(); + if (!response.equals("")) { + logger.info("Altona protocol violation - didn't start with an inital empty response: '{}'", response); + } + + // Get the new "Login: " prompt response + response = callback.getResponse(); + if (response.equals(RSP_LOGIN)) { + if (_config.getUserName() == null || _config.getUserName().trim().length() == 0) { + return "Atlona PRO3 has enabled Telnet/IP Login but no username was provided in the configuration."; + } + + // Send the username and wait for a ": " response + _session.sendCommand(_config.getUserName()); + } else { + return "Altona protocol violation - wasn't initially a command failure or login prompt: " + response; + } + + // We should have gotten the password response + response = callback.getResponse(); + + // Burn the empty response if we got one ( + if (response.equals("")) { + response = callback.getResponse(); + } + if (!response.equals(RSP_PASSWORD)) { + // If we got another login response, username wasn't valid + if (response.equals(RSP_LOGIN)) { + return "Username " + _config.getUserName() + " is not a valid user on the atlona"; + } + return "Altona protocol violation - invalid response to a login: " + response; + } + + // Make sure we have a password + if (_config.getPassword() == null || _config.getPassword().trim().length() == 0) { + return "Atlona PRO3 has enabled Telnet/IP Login but no password was provided in the configuration."; + } + + // Send the password + _session.sendCommand(_config.getPassword()); + response = callback.getResponse(); + + // At this point, we don't know if we received a + // 1) "\r\n" and waiting for a command + // or 2) "\r\nLogin: " if the password is invalid + // Send an invalid command to see if we get the failed command response + + // First make sure we had an empty response (the "\r\n" part) + if (!response.equals("")) { + logger.info("Altona protocol violation - not an empty response after password: '{}'", response); + } + + // Now send an invalid command + _session.sendCommand(NOTVALID_USER_OR_CMD); + + // If we get an invalid command response - we are logged in + response = callback.getResponse(); + if (response.startsWith(RSP_FAILED)) { + postLogin(); + return null; + } + + // Nope - password invalid + return "Password was invalid - please check your atlona setup"; + } + + /** + * Post successful login stuff - mark us online and refresh from the switch + */ + private void postLogin() { + logger.debug("Atlona switch now connected"); + _session.clearListeners(); + _session.addListener(new NormalResponseCallback()); + _callback.statusChanged(ThingStatus.ONLINE, ThingStatusDetail.NONE, null); + + // Set broadcast to on to receive notifications when + // routing changes (via the webpage, or presets or IR, etc) + sendCommand(CMD_BROADCAST_ON); + + // setup the most likely state of these switches (there is no protocol to get them) + refreshAll(); + } + + /** + * Returns the callback being used by this handler + * + * @return a non-null {@link AtlonaHandlerCallback} + */ + AtlonaHandlerCallback getCallback() { + return _callback; + } + + /** + * Pings the server with an (invalid) ping command to keep the connection alive + */ + void ping() { + sendCommand(CMD_PING); + } + + /** + * Refreshes the state from the switch itself. This will retrieve all the state (that we can get) from the switch. + */ + void refreshAll() { + logger.debug("Refreshing matrix state"); + if (_version == null) { + refreshVersion(); + } else { + _callback.setProperty(AtlonaPro3Constants.PROPERTY_VERSION, _version); + } + + if (_modelType == null) { + refreshType(); + } else { + _callback.setProperty(AtlonaPro3Constants.PROPERTY_TYPE, _modelType); + } + + refreshPower(); + refreshAllPortStatuses(); + + final int nbrPowerPorts = _capabilities.getNbrPowerPorts(); + for (int x = 1; x <= nbrPowerPorts; x++) { + refreshPortPower(x); + } + + final int nbrAudioPorts = _capabilities.getNbrAudioPorts(); + for (int x = 1; x <= nbrAudioPorts; x++) { + refreshVolumeStatus(x); + refreshVolumeMute(x); + } + + for (int x : _capabilities.getHdmiPorts()) { + refreshPortStatus(x); + } + } + + /** + * Sets the power to the switch + * + * @param on true if on, false otherwise + */ + void setPower(boolean on) { + sendCommand(on ? CMD_POWERON : CMD_POWEROFF); + } + + /** + * Queries the switch about it's power state + */ + void refreshPower() { + sendCommand(CMD_POWER_STATUS); + } + + /** + * Queries the switch about it's version (firmware) + */ + void refreshVersion() { + sendCommand(CMD_VERSION); + } + + /** + * Queries the switch about it's type (model) + */ + void refreshType() { + sendCommand(CMD_TYPE); + } + + /** + * Sets whether the front panel is locked or not + * + * @param locked true if locked, false otherwise + */ + void setPanelLock(boolean locked) { + sendCommand(locked ? CMD_PANELLOCK : CMD_PANELUNLOCK); + } + + /** + * Resets all ports back to their default state. + */ + void resetAllPorts() { + sendCommand(CMD_PORT_RESETALL); + } + + /** + * Sets whether the specified port is powered (i.e. outputing). + * + * @param portNbr a greater than zero port number + * @param on true if powered. + */ + void setPortPower(int portNbr, boolean on) { + if (portNbr <= 0) { + throw new IllegalArgumentException("portNbr must be greater than 0"); + } + sendCommand(String.format(CMD_PORT_POWER_FORMAT, portNbr, on ? "on" : "off")); + } + + /** + * Refreshes whether the specified port is powered (i.e. outputing). + * + * @param portNbr a greater than zero port number + */ + void refreshPortPower(int portNbr) { + if (portNbr <= 0) { + throw new IllegalArgumentException("portNbr must be greater than 0"); + } + sendCommand(String.format(CMD_PORT_POWER_FORMAT, portNbr, "sta")); + } + + /** + * Sets all the output ports to the specified input port. + * + * @param portNbr a greater than zero port number + */ + void setPortAll(int portNbr) { + if (portNbr <= 0) { + throw new IllegalArgumentException("portNbr must be greater than 0"); + } + sendCommand(String.format(CMD_PORT_ALL_FORMAT, portNbr)); + } + + /** + * Sets the input port number to the specified output port number. + * + * @param inPortNbr a greater than zero port number + * @param outPortNbr a greater than zero port number + */ + void setPortSwitch(int inPortNbr, int outPortNbr) { + if (inPortNbr <= 0) { + throw new IllegalArgumentException("inPortNbr must be greater than 0"); + } + if (outPortNbr <= 0) { + throw new IllegalArgumentException("outPortNbr must be greater than 0"); + } + sendCommand(String.format(CMD_PORT_SWITCH_FORMAT, inPortNbr, outPortNbr)); + } + + /** + * Sets the hdmi port number to mirror the specified output port number. + * + * @param hdmiPortNbr a greater than zero port number + * @param outPortNbr a greater than zero port number + */ + void setPortMirror(int hdmiPortNbr, int outPortNbr) { + if (hdmiPortNbr <= 0) { + throw new IllegalArgumentException("hdmiPortNbr must be greater than 0"); + } + if (outPortNbr <= 0) { + throw new IllegalArgumentException("outPortNbr must be greater than 0"); + } + + if (_capabilities.getHdmiPorts().contains(hdmiPortNbr)) { + sendCommand(String.format(CMD_PORT_MIRROR_FORMAT, hdmiPortNbr, outPortNbr)); + } else { + logger.info("Trying to set port mirroring on a non-hdmi port: {}", hdmiPortNbr); + } + } + + /** + * Disabled mirroring on the specified hdmi port number. + * + * @param hdmiPortNbr a greater than zero port number + * @param outPortNbr a greater than zero port number + */ + void removePortMirror(int hdmiPortNbr) { + if (hdmiPortNbr <= 0) { + throw new IllegalArgumentException("hdmiPortNbr must be greater than 0"); + } + + if (_capabilities.getHdmiPorts().contains(hdmiPortNbr)) { + sendCommand(String.format(CMD_PORT_UNMIRROR_FORMAT, hdmiPortNbr)); + } else { + logger.info("Trying to remove port mirroring on a non-hdmi port: {}", hdmiPortNbr); + } + } + + /** + * Sets the volume level on the specified audio port. + * + * @param portNbr a greater than zero port number + * @param level a volume level in decibels (must range from -79 to +15) + */ + void setVolume(int portNbr, double level) { + if (portNbr <= 0) { + throw new IllegalArgumentException("portNbr must be greater than 0"); + } + if (level < -79 || level > 15) { + throw new IllegalArgumentException("level must be between -79 to +15"); + } + sendCommand(String.format(CMD_VOLUME_FORMAT, portNbr, level)); + } + + /** + * Refreshes the volume level for the given audio port. + * + * @param portNbr a greater than zero port number + */ + void refreshVolumeStatus(int portNbr) { + if (portNbr <= 0) { + throw new IllegalArgumentException("portNbr must be greater than 0"); + } + sendCommand(String.format(CMD_VOLUME_FORMAT, portNbr, "sta")); + } + + /** + * Refreshes the specified hdmi port's mirroring status + * + * @param hdmiPortNbr a greater than zero hdmi port number + */ + void refreshPortMirror(int hdmiPortNbr) { + if (hdmiPortNbr <= 0) { + throw new IllegalArgumentException("hdmiPortNbr must be greater than 0"); + } + sendCommand(String.format(CMD_PORT_MIRROR_STATUS_FORMAT, hdmiPortNbr)); + } + + /** + * Mutes/Unmutes the specified audio port. + * + * @param portNbr a greater than zero port number + * @param mute true to mute, false to unmute + */ + void setVolumeMute(int portNbr, boolean mute) { + if (portNbr <= 0) { + throw new IllegalArgumentException("portNbr must be greater than 0"); + } + sendCommand(String.format(CMD_VOLUME_MUTE_FORMAT, portNbr, mute ? "on" : "off")); + } + + /** + * Refreshes the volume mute for the given audio port. + * + * @param portNbr a greater than zero port number + */ + void refreshVolumeMute(int portNbr) { + if (portNbr <= 0) { + throw new IllegalArgumentException("portNbr must be greater than 0"); + } + sendCommand(String.format(CMD_VOLUME_MUTE_FORMAT, portNbr, "sta")); + } + + /** + * Turn on/off the front panel IR. + * + * @param on true for on, false otherwise + */ + void setIrOn(boolean on) { + sendCommand(on ? CMD_IRON : CMD_IROFF); + } + + /** + * Refreshes the input port setting on the specified output port. + * + * @param portNbr a greater than zero port number + */ + void refreshPortStatus(int portNbr) { + if (portNbr <= 0) { + throw new IllegalArgumentException("portNbr must be greater than 0"); + } + sendCommand(String.format(CMD_PORT_STATUS_FORMAT, portNbr)); + } + + /** + * Refreshes all of the input port settings for all of the output ports. + */ + private void refreshAllPortStatuses() { + sendCommand(CMD_PORT_STATUS); + } + + /** + * Saves the current Input/Output scheme to the specified preset number. + * + * @param presetNbr a greater than 0 preset number + */ + void saveIoSettings(int presetNbr) { + if (presetNbr <= 0) { + throw new IllegalArgumentException("presetNbr must be greater than 0"); + } + sendCommand(String.format(CMD_SAVEIO_FORMAT, presetNbr)); + } + + /** + * Recalls the Input/Output scheme for the specified preset number. + * + * @param presetNbr a greater than 0 preset number + */ + void recallIoSettings(int presetNbr) { + if (presetNbr <= 0) { + throw new IllegalArgumentException("presetNbr must be greater than 0"); + } + sendCommand(String.format(CMD_RECALLIO_FORMAT, presetNbr)); + } + + /** + * Clears the Input/Output scheme for the specified preset number. + * + * @param presetNbr a greater than 0 preset number + */ + void clearIoSettings(int presetNbr) { + if (presetNbr <= 0) { + throw new IllegalArgumentException("presetNbr must be greater than 0"); + } + sendCommand(String.format(CMD_CLEARIO_FORMAT, presetNbr)); + } + + /** + * Resets the matrix back to defaults. + */ + void resetMatrix() { + sendCommand(CMD_MATRIX_RESET); + } + + /** + * Sends the command and puts the thing into {@link ThingStatus#OFFLINE} if an IOException occurs + * + * @param command a non-null, non-empty command to send + */ + private void sendCommand(String command) { + if (command == null) { + throw new IllegalArgumentException("command cannot be null"); + } + if (command.trim().length() == 0) { + throw new IllegalArgumentException("command cannot be empty"); + } + try { + _session.sendCommand(command); + } catch (IOException e) { + _callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Exception occurred sending to Atlona: " + e); + } + } + + /** + * Handles the switch power response. The first matching group should be "on" or "off" + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handlePowerResponse(Matcher m, String resp) { + if (m == null) { + throw new IllegalArgumentException("m (matcher) cannot be null"); + } + if (m.groupCount() == 1) { + switch (m.group(1)) { + case "ON": + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY, + AtlonaPro3Constants.CHANNEL_POWER), OnOffType.ON); + break; + case "OFF": + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY, + AtlonaPro3Constants.CHANNEL_POWER), OnOffType.OFF); + break; + default: + logger.warn("Invalid power response: '{}'", resp); + } + } else { + logger.warn("Invalid power response: '{}'", resp); + } + } + + /** + * Handles the version (firmware) response. The first matching group should be the version + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handleVersionResponse(Matcher m, String resp) { + if (m == null) { + throw new IllegalArgumentException("m (matcher) cannot be null"); + } + if (m.groupCount() == 1) { + _version = m.group(1); + _callback.setProperty(AtlonaPro3Constants.PROPERTY_VERSION, _version); + } else { + logger.warn("Invalid version response: '{}'", resp); + } + } + + /** + * Handles the type (model) response. The first matching group should be the type. + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handleTypeResponse(Matcher m, String resp) { + if (m == null) { + throw new IllegalArgumentException("m (matcher) cannot be null"); + } + if (m.groupCount() == 1) { + _modelType = resp; + _callback.setProperty(AtlonaPro3Constants.PROPERTY_TYPE, _modelType); + } else { + logger.warn("Invalid Type response: '{}'", resp); + } + } + + /** + * Handles the panel lock response. The response is only on or off. + * + * @param resp the possibly null, possibly empty actual response + */ + private void handlePanelLockResponse(String resp) { + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY, + AtlonaPro3Constants.CHANNEL_PANELLOCK), RSP_LOCK.equals(resp) ? OnOffType.ON : OnOffType.OFF); + } + + /** + * Handles the port power response. The first two groups should be the port nbr and either "on" or "off" + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handlePortPowerResponse(Matcher m, String resp) { + if (m == null) { + throw new IllegalArgumentException("m (matcher) cannot be null"); + } + if (m.groupCount() == 2) { + try { + int portNbr = Integer.parseInt(m.group(1)); + switch (m.group(2)) { + case "on": + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT, + portNbr, AtlonaPro3Constants.CHANNEL_PORTPOWER), OnOffType.ON); + break; + case "off": + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT, + portNbr, AtlonaPro3Constants.CHANNEL_PORTPOWER), OnOffType.OFF); + break; + default: + logger.warn("Invalid port power response: '{}'", resp); + } + } catch (NumberFormatException e) { + logger.warn("Invalid port power (can't parse number): '{}'", resp); + } + } else { + logger.warn("Invalid port power response: '{}'", resp); + } + } + + /** + * Handles the port all response. Simply calls {@link #refreshAllPortStatuses()} + * + * @param resp ignored + */ + private void handlePortAllResponse(String resp) { + refreshAllPortStatuses(); + } + + /** + * Handles the port output response. This matcher can have multiple groups separated by commas. Find each group and + * that group should have two groups within - an input port nbr and an output port number + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handlePortOutputResponse(Matcher m, String resp) { + if (m == null) { + throw new IllegalArgumentException("m (matcher) cannot be null"); + } + + m.reset(); + while (m.find()) { + try { + int inPort = Integer.parseInt(m.group(1)); + int outPort = Integer.parseInt(m.group(2)); + + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT, outPort, + AtlonaPro3Constants.CHANNEL_PORTOUTPUT), new DecimalType(inPort)); + } catch (NumberFormatException e) { + logger.warn("Invalid port output response (can't parse number): '{}'", resp); + } + } + } + + /** + * Handles the mirror response. The matcher should have two groups - an hdmi port number and an output port number. + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handleMirrorResponse(Matcher m, String resp) { + if (m == null) { + throw new IllegalArgumentException("m (matcher) cannot be null"); + } + if (m.groupCount() == 3) { + try { + int hdmiPortNbr = Integer.parseInt(m.group(1)); + + // could be "off" (if mirror off), "on"/"Out" (with 3rd group representing out) + String oper = StringUtils.trimToEmpty(m.group(2)).toLowerCase(); + + if (oper.equals("off")) { + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR, + hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRRORENABLED), OnOffType.OFF); + } else { + int outPortNbr = Integer.parseInt(m.group(3)); + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR, + hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRROR), new DecimalType(outPortNbr)); + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR, + hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRRORENABLED), OnOffType.ON); + } + } catch (NumberFormatException e) { + logger.warn("Invalid mirror response (can't parse number): '{}'", resp); + } + } else { + logger.warn("Invalid mirror response: '{}'", resp); + } + } + + /** + * Handles the unmirror response. The first group should contain the hdmi port number + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handleUnMirrorResponse(Matcher m, String resp) { + if (m == null) { + throw new IllegalArgumentException("m (matcher) cannot be null"); + } + if (m.groupCount() == 1) { + try { + int hdmiPortNbr = Integer.parseInt(m.group(1)); + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR, + hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRROR), new DecimalType(0)); + } catch (NumberFormatException e) { + logger.warn("Invalid unmirror response (can't parse number): '{}'", resp); + } + } else { + logger.warn("Invalid unmirror response: '{}'", resp); + } + } + + /** + * Handles the volume response. The first two group should be the audio port number and the level + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handleVolumeResponse(Matcher m, String resp) { + if (m == null) { + throw new IllegalArgumentException("m (matcher) cannot be null"); + } + if (m.groupCount() == 2) { + try { + int portNbr = Integer.parseInt(m.group(1)); + double level = Double.parseDouble(m.group(2)); + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME, portNbr, + AtlonaPro3Constants.CHANNEL_VOLUME), new DecimalType(level)); + } catch (NumberFormatException e) { + logger.warn("Invalid volume response (can't parse number): '{}'", resp); + } + } else { + logger.warn("Invalid volume response: '{}'", resp); + } + } + + /** + * Handles the volume mute response. The first two group should be the audio port number and either "on" or "off + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handleVolumeMuteResponse(Matcher m, String resp) { + if (m == null) { + throw new IllegalArgumentException("m (matcher) cannot be null"); + } + if (m.groupCount() == 2) { + try { + int portNbr = Integer.parseInt(m.group(1)); + switch (m.group(2)) { + case "on": + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME, + portNbr, AtlonaPro3Constants.CHANNEL_VOLUME_MUTE), OnOffType.ON); + break; + case "off": + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME, + portNbr, AtlonaPro3Constants.CHANNEL_VOLUME_MUTE), OnOffType.OFF); + break; + default: + logger.warn("Invalid volume mute response: '{}'", resp); + } + } catch (NumberFormatException e) { + logger.warn("Invalid volume mute (can't parse number): '{}'", resp); + } + } else { + logger.warn("Invalid volume mute response: '{}'", resp); + } + } + + /** + * Handles the IR Response. The response is either on or off + * + * @param resp the possibly null, possibly empty actual response + */ + private void handleIrLockResponse(String resp) { + _callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY, + AtlonaPro3Constants.CHANNEL_IRENABLE), RSP_IRON.equals(resp) ? OnOffType.ON : OnOffType.OFF); + } + + /** + * Handles the Save IO Response. Should have one group specifying the preset number + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handleSaveIoResponse(Matcher m, String resp) { + // nothing to handle + } + + /** + * Handles the Recall IO Response. Should have one group specifying the preset number. After updating the Recall + * State, we refresh all the ports via {@link #refreshAllPortStatuses()}. + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handleRecallIoResponse(Matcher m, String resp) { + refreshAllPortStatuses(); + } + + /** + * Handles the Clear IO Response. Should have one group specifying the preset number. + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handleClearIoResponse(Matcher m, String resp) { + // nothing to handle + } + + /** + * Handles the broadcast Response. Should have one group specifying the status. + * + * @param m the non-null {@link Matcher} that matched the response + * @param resp the possibly null, possibly empty actual response + */ + private void handleBroadcastResponse(Matcher m, String resp) { + // nothing to handle + } + + /** + * Handles the matrix reset response. The matrix will go offline immediately on a reset. + * + * @param resp the possibly null, possibly empty actual response + */ + private void handleMatrixResetResponse(String resp) { + if (RSP_MATRIX_RESET.equals(resp)) { + _callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "System is rebooting due to matrix reset"); + } + } + + /** + * Handles a command failure - we simply log the response as an error + * + * @param resp the possibly null, possibly empty actual response + */ + private void handleCommandFailure(String resp) { + logger.info(resp); + } + + /** + * This callback is our normal response callback. Should be set into the {@link SocketSession} after the login + * process to handle normal responses. + * + * @author Tim Roberts + * + */ + private class NormalResponseCallback implements SocketSessionListener { + + @Override + public void responseReceived(String response) { + if (response == null || response == "") { + return; + } + + if (RSP_PING.equals(response)) { + // ignore + return; + } + + Matcher m; + + m = _portStatusPattern.matcher(response); + if (m.find()) { + handlePortOutputResponse(m, response); + return; + } + + m = _powerStatusPattern.matcher(response); + if (m.matches()) { + handlePowerResponse(m, response); + return; + } + + m = _versionPattern.matcher(response); + if (m.matches()) { + handleVersionResponse(m, response); + return; + } + + m = _typePattern.matcher(response); + if (m.matches()) { + handleTypeResponse(m, response); + return; + } + + m = _portPowerPattern.matcher(response); + if (m.matches()) { + handlePortPowerResponse(m, response); + return; + } + + m = _volumePattern.matcher(response); + if (m.matches()) { + handleVolumeResponse(m, response); + return; + } + + m = _volumeMutePattern.matcher(response); + if (m.matches()) { + handleVolumeMuteResponse(m, response); + return; + } + + m = _portAllPattern.matcher(response); + if (m.matches()) { + handlePortAllResponse(response); + return; + } + + m = _portMirrorPattern.matcher(response); + if (m.matches()) { + handleMirrorResponse(m, response); + return; + } + + m = _portUnmirrorPattern.matcher(response); + if (m.matches()) { + handleUnMirrorResponse(m, response); + return; + } + + m = _saveIoPattern.matcher(response); + if (m.matches()) { + handleSaveIoResponse(m, response); + return; + } + + m = _recallIoPattern.matcher(response); + if (m.matches()) { + handleRecallIoResponse(m, response); + return; + } + + m = _clearIoPattern.matcher(response); + if (m.matches()) { + handleClearIoResponse(m, response); + return; + } + + m = _broadCastPattern.matcher(response); + if (m.matches()) { + handleBroadcastResponse(m, response); + return; + } + + if (RSP_IRON.equals(response) || RSP_IROFF.equals(response)) { + handleIrLockResponse(response); + return; + } + + if (RSP_ALL.equals(response)) { + handlePortAllResponse(response); + return; + } + + if (RSP_LOCK.equals(response) || RSP_UNLOCK.equals(response)) { + handlePanelLockResponse(response); + return; + } + + if (RSP_MATRIX_RESET.equals(response)) { + handleMatrixResetResponse(response); + return; + } + + if (response.startsWith(RSP_FAILED)) { + handleCommandFailure(response); + return; + } + + logger.info("Unhandled response: {}", response); + } + + @Override + public void responseException(Exception e) { + _callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Exception occurred reading from Atlona: " + e); + } + + } + + /** + * Special callback used during the login process to not dispatch the responses to this class but rather give them + * back at each call to {@link NoDispatchingCallback#getResponse()} + * + * @author Tim Roberts + * + */ + private class NoDispatchingCallback implements SocketSessionListener { + + /** + * Cache of responses that have occurred + */ + private BlockingQueue _responses = new ArrayBlockingQueue(5); + + /** + * Will return the next response from {@link #_responses}. If the response is an exception, that exception will + * be thrown instead. + * + * @return a non-null, possibly empty response + * @throws Exception an exception if one occurred during reading + */ + String getResponse() throws Exception { + final Object lastResponse = _responses.poll(5, TimeUnit.SECONDS); + if (lastResponse instanceof String) { + return (String) lastResponse; + } else if (lastResponse instanceof Exception) { + throw (Exception) lastResponse; + } else if (lastResponse == null) { + throw new Exception("Didn't receive response in time"); + } else { + return lastResponse.toString(); + } + } + + @Override + public void responseReceived(String response) { + try { + _responses.put(response); + } catch (InterruptedException e) { + } + } + + @Override + public void responseException(Exception e) { + try { + _responses.put(e); + } catch (InterruptedException e1) { + } + + } + + } +} diff --git a/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Utilities.java b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Utilities.java new file mode 100644 index 0000000000000..1e06fc6d8a1d7 --- /dev/null +++ b/addons/binding/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/pro3/AtlonaPro3Utilities.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2017 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.atlona.internal.pro3; + +public class AtlonaPro3Utilities { + /** + * Helper method to create a channel id from a group with no port number attached + * + * @param group a group name + * @param channelId the channel id + * @return group + "#" + channelId + */ + public static String createChannelID(String group, String channelId) { + return group + "#" + channelId; + } + + /** + * Helper method to create a channel id from a group, port number and channel id + * + * @param group the group name + * @param portNbr the port number + * @param channelId the channel id + * @return group + portNbr + "#" + channelId + */ + public static String createChannelID(String group, int portNbr, String channelId) { + return group + portNbr + "#" + channelId; + } +} diff --git a/addons/binding/pom.xml b/addons/binding/pom.xml index 4e984997dc1b1..1d2f017dca779 100644 --- a/addons/binding/pom.xml +++ b/addons/binding/pom.xml @@ -20,6 +20,7 @@ org.openhab.binding.allplay org.openhab.binding.amazondashbutton org.openhab.binding.astro + org.openhab.binding.atlona org.openhab.binding.autelis org.openhab.binding.avmfritz org.openhab.binding.boschindego diff --git a/features/openhab-addons/src/main/feature/feature.xml b/features/openhab-addons/src/main/feature/feature.xml index ff6c437ea64bb..1e44e12256c06 100644 --- a/features/openhab-addons/src/main/feature/feature.xml +++ b/features/openhab-addons/src/main/feature/feature.xml @@ -33,6 +33,11 @@ mvn:org.openhab.binding/org.openhab.binding.astro/${project.version} + + openhab-runtime-base + mvn:org.openhab.binding/org.openhab.binding.atlona/${project.version} + + openhab-runtime-base openhab-transport-upnp