added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.binding.nuvo/.classpath
Normal file
32
bundles/org.openhab.binding.nuvo/.classpath
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.nuvo/.project
Normal file
23
bundles/org.openhab.binding.nuvo/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.nuvo</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
13
bundles/org.openhab.binding.nuvo/NOTICE
Normal file
13
bundles/org.openhab.binding.nuvo/NOTICE
Normal file
@@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
397
bundles/org.openhab.binding.nuvo/README.md
Normal file
397
bundles/org.openhab.binding.nuvo/README.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# Nuvo Grand Concerto & Essentia G Binding
|
||||
|
||||

|
||||
|
||||
This binding can be used to control the Nuvo Grand Concerto or Essentia G whole house multi-zone amplifier.
|
||||
Up to 20 keypad zones can be controlled when zone expansion modules are used (if not all zones on the amp are used they can be excluded via configuration).
|
||||
|
||||
The binding supports two different kinds of connections:
|
||||
|
||||
* serial connection,
|
||||
* serial over IP connection
|
||||
|
||||
For users without a serial connector on the server side, you can use a serial to USB adapter.
|
||||
|
||||
You don't need to have your Grand Concerto or Essentia G whole house amplifier device directly connected to your openHAB server.
|
||||
You can connect it for example to a Raspberry Pi and use [ser2net Linux tool](https://sourceforge.net/projects/ser2net/) to make the serial connection available on LAN (serial over IP).
|
||||
|
||||
## Supported Things
|
||||
|
||||
There is exactly one supported thing type, which represents the amplifier controller.
|
||||
It has the `amplifier` id.
|
||||
|
||||
## Discovery
|
||||
|
||||
Discovery is not supported.
|
||||
You have to add all things manually.
|
||||
|
||||
## Binding Configuration
|
||||
|
||||
There are no overall binding configuration settings that need to be set.
|
||||
All settings are through thing configuration parameters.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
The thing has the following configuration parameters:
|
||||
|
||||
| Parameter Label | Parameter ID | Description | Accepted values |
|
||||
|-------------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------|------------------------|
|
||||
| Serial Port | serialPort | Serial port to use for connecting to the Nuvo whole house amplifier device | a comm port name |
|
||||
| Address | host | Host name or IP address of the machine connected to the Nuvo whole house amplifier device (serial over IP) | host name or ip |
|
||||
| Port | port | Communication port (serial over IP). | ip port number |
|
||||
| Number of Zones | numZones | (Optional) Number of zones on the amplifier to utilize in the binding (up to 20 zones when zone expansion modules are used) | (1-20; default 6) |
|
||||
| Sync Clock on GConcerto | clockSync | (Optional) If set to true, the binding will sync the internal clock on the Grand Concerto to match the openHAB host's system clock | Boolean; default false |
|
||||
|
||||
Some notes:
|
||||
|
||||
* If a zone has a maximum volume limit configured by the Nuvo configurator, the volume slider will automatically drop back to that level if set above the configured limit.
|
||||
* Source display_line1 thru 4 can only be updated on non NuvoNet sources.
|
||||
* The track_position channel does not update continuously for NuvoNet sources. It only changes when the track changes or playback is paused/unpaused.
|
||||
|
||||
* On Linux, you may get an error stating the serial port cannot be opened when the Nuvo binding tries to load.
|
||||
* You can get around this by adding the `openhab` user to the `dialout` group like this: `usermod -a -G dialout openhab`.
|
||||
* Also on Linux you may have issues with the USB if using two serial USB devices e.g. Nuvo and RFXcom. See the [general documentation about serial port configuration](/docs/administration/serial.html) for more on symlinking the USB ports.
|
||||
* Here is an example of ser2net.conf you can use to share your serial port /dev/ttyUSB0 on IP port 4444 using [ser2net Linux tool](https://sourceforge.net/projects/ser2net/) (take care, the baud rate is specific to the Nuvo amplifier):
|
||||
|
||||
```
|
||||
4444:raw:0:/dev/ttyUSB0:57600 8DATABITS NONE 1STOPBIT LOCAL
|
||||
```
|
||||
|
||||
## Channels
|
||||
|
||||
The following channels are available:
|
||||
|
||||
| Channel ID | Item Type | Description |
|
||||
|--------------------------------------|-------------|---------------------------------------------------------------------------------------------------------------|
|
||||
| system#alloff | Switch | Turn all zones off simultaneously |
|
||||
| system#allmute | Switch | Mute or unmute all zones simultaneously |
|
||||
| system#page | Switch | Turn on or off the Page All Zones feature (while on the amplifier switches to source 6) |
|
||||
| zoneN#power (where N= 1-20) | Switch | Turn the power for a zone on or off |
|
||||
| zoneN#source (where N= 1-20) | Number | Select the source input for a zone (1-6) |
|
||||
| zoneN#volume (where N= 1-20) | Dimmer | Control the volume for a zone (0-100%) [translates to 0-79] |
|
||||
| zoneN#mute (where N= 1-20) | Switch | Mute or unmute a zone |
|
||||
| zoneN#control (where N= 1-20) | Player | Simulate pressing the transport control buttons on the keypad e.g. play/pause/next/previous |
|
||||
| zoneN#treble (where N= 1-20) | Number | Adjust the treble control for a zone (-18 to 18 [in increments of 2]) -18=none, 0=flat, 18=full |
|
||||
| zoneN#bass (where N= 1-20) | Number | Adjust the bass control for a zone (-18 to 18 [in increments of 2]) -18=none, 0=flat, 18=full |
|
||||
| zoneN#balance (where N= 1-20) | Number | Adjust the balance control for a zone (-18 to 18 [in increments of 2]) -18=left, 0=center, 18=right |
|
||||
| zoneN#loudness (where N= 1-20) | Switch | Turn on or off the loudness compensation setting for the zone |
|
||||
| zoneN#dnd (where N= 1-20) | Switch | Turn on or off the Do Not Disturb for the zone (for when the amplifiers's Page All Zones feature is activated)|
|
||||
| zoneN#lock (where N= 1-20) | Contact | Indicates if this zone is currently locked |
|
||||
| zoneN#party (where N= 1-20) | Switch | Turn on or off the party mode feature with this zone as the host |
|
||||
| sourceN#display_line1 (where N= 1-6) | String | 1st line of text being displayed on the keypad. Can be updated for a non NuvoNet source |
|
||||
| sourceN#display_line2 (where N= 1-6) | String | 2nd line of text being displayed on the keypad. Can be updated for a non NuvoNet source |
|
||||
| sourceN#display_line3 (where N= 1-6) | String | 3rd line of text being displayed on the keypad. Can be updated for a non NuvoNet source |
|
||||
| sourceN#display_line4 (where N= 1-6) | String | 4th line of text being displayed on the keypad. Can be updated for a non NuvoNet source |
|
||||
| sourceN#play_mode (where N= 1-6) | String | The current playback mode of the source, ie: Playing, Paused, etc. (ReadOnly) See rules example for updating |
|
||||
| sourceN#track_length (where N= 1-6) | Number:Time | The total running time of the current playing track (ReadOnly) See rules example for updating |
|
||||
| sourceN#track_position (where N= 1-6)| Number:Time | The running time elapsed of the current playing track (ReadOnly) See rules example for updating |
|
||||
| sourceN#button_press (where N= 1-6) | String | Indicates the last button pressed on the keypad for a non NuvoNet source (ReadOnly) |
|
||||
|
||||
## Full Example
|
||||
|
||||
nuvo.things:
|
||||
|
||||
```java
|
||||
//serial port connection
|
||||
nuvo:amplifier:myamp "Nuvo WHA" [ serialPort="COM5", numZones=6, clockSync=false]
|
||||
|
||||
// serial over IP connection
|
||||
nuvo:amplifier:myamp "Nuvo WHA" [ host="192.168.0.10", port=4444, numZones=6, clockSync=false]
|
||||
|
||||
```
|
||||
|
||||
nuvo.items:
|
||||
|
||||
```java
|
||||
// system
|
||||
Switch nuvo_system_alloff "All Zones Off" { channel="nuvo:amplifier:myamp:system#alloff" }
|
||||
Switch nuvo_system_allmute "All Zones Mute" { channel="nuvo:amplifier:myamp:system#allmute" }
|
||||
Switch nuvo_system_page "Page All Zones" { channel="nuvo:amplifier:myamp:system#page" }
|
||||
|
||||
// zones
|
||||
Switch nuvo_z1_power "Power" { channel="nuvo:amplifier:myamp:zone1#power" }
|
||||
Number nuvo_z1_source "Source Input [%s]" { channel="nuvo:amplifier:myamp:zone1#source" }
|
||||
Dimmer nuvo_z1_volume "Volume [%d %%]" { channel="nuvo:amplifier:myamp:zone1#volume" }
|
||||
Switch nuvo_z1_mute "Mute" { channel="nuvo:amplifier:myamp:zone1#mute" }
|
||||
Player nuvo_z1_control "Control" { channel="nuvo:amplifier:myamp:zone1#control" }
|
||||
Number nuvo_z1_treble "Treble Adjustment [%s]" { channel="nuvo:amplifier:myamp:zone1#treble" }
|
||||
Number nuvo_z1_bass "Bass Adjustment [%s]" { channel="nuvo:amplifier:myamp:zone1#bass" }
|
||||
Number nuvo_z1_balance "Balance Adjustment [%s]" { channel="nuvo:amplifier:myamp:zone1#balance" }
|
||||
Switch nuvo_z1_loudness "Loudness" { channel="nuvo:amplifier:myamp:zone1#loudness" }
|
||||
Switch nuvo_z1_dnd "Do Not Disturb" { channel="nuvo:amplifier:myamp:zone1#dnd" }
|
||||
Switch nuvo_z1_lock "Zone Locked [%s]" { channel="nuvo:amplifier:myamp:zone1#lock" }
|
||||
Switch nuvo_z1_party "Party Mode" { channel="nuvo:amplifier:myamp:zone1#party" }
|
||||
|
||||
// > repeat for zones 2-20 (substitute z1 and zone1) < //
|
||||
|
||||
// sources
|
||||
String nuvo_s1_display_line1 "Line 1: [%s]" { channel="nuvo:amplifier:myamp:source1#display_line1" }
|
||||
String nuvo_s1_display_line2 "Line 2: [%s]" { channel="nuvo:amplifier:myamp:source1#display_line2" }
|
||||
String nuvo_s1_display_line3 "Line 3: [%s]" { channel="nuvo:amplifier:myamp:source1#display_line3" }
|
||||
String nuvo_s1_display_line4 "Line 4: [%s]" { channel="nuvo:amplifier:myamp:source1#display_line4" }
|
||||
String nuvo_s1_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:source1#play_mode" }
|
||||
Number:Time nuvo_s1_track_length "Track Length: [%s s]" { channel="nuvo:amplifier:myamp:source1#track_length" }
|
||||
Number:Time nuvo_s1_track_position "Track Position: [%s s]" { channel="nuvo:amplifier:myamp:source1#track_position" }
|
||||
String nuvo_s1_button_press "Button: [%s]" { channel="nuvo:amplifier:myamp:source1#button_press" }
|
||||
|
||||
String nuvo_s2_display_line1 "Line 1: [%s]" { channel="nuvo:amplifier:myamp:source2#display_line1" }
|
||||
String nuvo_s2_display_line2 "Line 2: [%s]" { channel="nuvo:amplifier:myamp:source2#display_line2" }
|
||||
String nuvo_s2_display_line3 "Line 3: [%s]" { channel="nuvo:amplifier:myamp:source2#display_line3" }
|
||||
String nuvo_s2_display_line4 "Line 4: [%s]" { channel="nuvo:amplifier:myamp:source2#display_line4" }
|
||||
String nuvo_s2_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:source2#play_mode" }
|
||||
Number:Time nuvo_s2_track_length "Track Length: [%s s]" { channel="nuvo:amplifier:myamp:source2#track_length" }
|
||||
Number:Time nuvo_s2_track_position "Track Position: [%s s]" { channel="nuvo:amplifier:myamp:source2#track_position" }
|
||||
String nuvo_s2_button_press "Button: [%s]" { channel="nuvo:amplifier:myamp:source2#button_press" }
|
||||
|
||||
String nuvo_s3_display_line1 "Line 1: [%s]" { channel="nuvo:amplifier:myamp:source3#display_line1" }
|
||||
String nuvo_s3_display_line2 "Line 2: [%s]" { channel="nuvo:amplifier:myamp:source3#display_line2" }
|
||||
String nuvo_s3_display_line3 "Line 3: [%s]" { channel="nuvo:amplifier:myamp:source3#display_line3" }
|
||||
String nuvo_s3_display_line4 "Line 4: [%s]" { channel="nuvo:amplifier:myamp:source3#display_line4" }
|
||||
String nuvo_s3_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:source3#play_mode" }
|
||||
Number:Time nuvo_s3_track_length "Track Length: [%s s]" { channel="nuvo:amplifier:myamp:source3#track_length" }
|
||||
Number:Time nuvo_s3_track_position "Track Position: [%s s]" { channel="nuvo:amplifier:myamp:source3#track_position" }
|
||||
String nuvo_s3_button_press "Button: [%s]" { channel="nuvo:amplifier:myamp:source3#button_press" }
|
||||
|
||||
String nuvo_s4_display_line1 "Line 1: [%s]" { channel="nuvo:amplifier:myamp:source4#display_line1" }
|
||||
String nuvo_s4_display_line2 "Line 2: [%s]" { channel="nuvo:amplifier:myamp:source4#display_line2" }
|
||||
String nuvo_s4_display_line3 "Line 3: [%s]" { channel="nuvo:amplifier:myamp:source4#display_line3" }
|
||||
String nuvo_s4_display_line4 "Line 4: [%s]" { channel="nuvo:amplifier:myamp:source4#display_line4" }
|
||||
String nuvo_s4_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:source4#play_mode" }
|
||||
Number:Time nuvo_s4_track_length "Track Length: [%s s]" { channel="nuvo:amplifier:myamp:source4#track_length" }
|
||||
Number:Time nuvo_s4_track_position "Track Position: [%s s]" { channel="nuvo:amplifier:myamp:source4#track_position" }
|
||||
String nuvo_s4_button_press "Button: [%s]" { channel="nuvo:amplifier:myamp:source4#button_press" }
|
||||
|
||||
String nuvo_s5_display_line1 "Line 1: [%s]" { channel="nuvo:amplifier:myamp:source5#display_line1" }
|
||||
String nuvo_s5_display_line2 "Line 2: [%s]" { channel="nuvo:amplifier:myamp:source5#display_line2" }
|
||||
String nuvo_s5_display_line3 "Line 3: [%s]" { channel="nuvo:amplifier:myamp:source5#display_line3" }
|
||||
String nuvo_s5_display_line4 "Line 4: [%s]" { channel="nuvo:amplifier:myamp:source5#display_line4" }
|
||||
String nuvo_s5_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:source5#play_mode" }
|
||||
Number:Time nuvo_s5_track_length "Track Length: [%s s]" { channel="nuvo:amplifier:myamp:source5#track_length" }
|
||||
Number:Time nuvo_s5_track_position "Track Position: [%s s]" { channel="nuvo:amplifier:myamp:source5#track_position" }
|
||||
String nuvo_s5_button_press "Button: [%s]" { channel="nuvo:amplifier:myamp:source5#button_press" }
|
||||
|
||||
String nuvo_s6_display_line1 "Line 1: [%s]" { channel="nuvo:amplifier:myamp:source6#display_line1" }
|
||||
String nuvo_s6_display_line2 "Line 2: [%s]" { channel="nuvo:amplifier:myamp:source6#display_line2" }
|
||||
String nuvo_s6_display_line3 "Line 3: [%s]" { channel="nuvo:amplifier:myamp:source6#display_line3" }
|
||||
String nuvo_s6_display_line4 "Line 4: [%s]" { channel="nuvo:amplifier:myamp:source6#display_line4" }
|
||||
String nuvo_s6_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:source6#play_mode" }
|
||||
Number:Time nuvo_s6_track_length "Track Length: [%s s]" { channel="nuvo:amplifier:myamp:source6#track_length" }
|
||||
Number:Time nuvo_s6_track_position "Track Position: [%s s]" { channel="nuvo:amplifier:myamp:source6#track_position" }
|
||||
String nuvo_s6_button_press "Button: [%s]" { channel="nuvo:amplifier:myamp:source6#button_press" }
|
||||
|
||||
```
|
||||
|
||||
nuvo.sitemap:
|
||||
|
||||
```perl
|
||||
sitemap nuvo label="Audio Control" {
|
||||
Frame label="System" {
|
||||
Switch item=nuvo_system_alloff mappings=[ON=" "]
|
||||
Switch item=nuvo_system_allmute
|
||||
Switch item=nuvo_system_page
|
||||
}
|
||||
|
||||
Frame label="Zone 1"
|
||||
Switch item=nuvo_z1_power visibility=[nuvo_z1_lock!="1"]
|
||||
Selection item=nuvo_z1_source visibility=[nuvo_z1_power==ON] icon="player"
|
||||
//Volume can be a Setpoint also
|
||||
Slider item=nuvo_z1_volume minValue=0 maxValue=100 step=1 visibility=[nuvo_z1_power==ON] icon="soundvolume"
|
||||
Switch item=nuvo_z1_mute visibility=[nuvo_z1_power==ON] icon="soundvolume_mute"
|
||||
Default item=nuvo_z1_control visibility=[nuvo_z1_power==ON]
|
||||
|
||||
Text item=nuvo_s1_display_line1 visibility=[nuvo_z1_source=="1"] icon="zoom"
|
||||
Text item=nuvo_s1_display_line2 visibility=[nuvo_z1_source=="1"] icon="zoom"
|
||||
Text item=nuvo_s1_display_line3 visibility=[nuvo_z1_source=="1"] icon="zoom"
|
||||
Text item=nuvo_s1_display_line4 visibility=[nuvo_z1_source=="1"] icon="zoom"
|
||||
Text item=nuvo_s1_play_mode visibility=[nuvo_z1_source=="1"] icon="player"
|
||||
Text item=nuvo_s1_track_length visibility=[nuvo_z1_source=="1"]
|
||||
Text item=nuvo_s1_track_position visibility=[nuvo_z1_source=="1"]
|
||||
Text item=nuvo_s1_button_press visibility=[nuvo_z1_source=="1"] icon="none"
|
||||
|
||||
Text item=nuvo_s2_display_line1 visibility=[nuvo_z1_source=="2"] icon="zoom"
|
||||
Text item=nuvo_s2_display_line2 visibility=[nuvo_z1_source=="2"] icon="zoom"
|
||||
Text item=nuvo_s2_display_line3 visibility=[nuvo_z1_source=="2"] icon="zoom"
|
||||
Text item=nuvo_s2_display_line4 visibility=[nuvo_z1_source=="2"] icon="zoom"
|
||||
Text item=nuvo_s2_play_mode visibility=[nuvo_z1_source=="2"] icon="player"
|
||||
Text item=nuvo_s2_track_length visibility=[nuvo_z1_source=="2"]
|
||||
Text item=nuvo_s2_track_position visibility=[nuvo_z1_source=="2"]
|
||||
Text item=nuvo_s2_button_press visibility=[nuvo_z1_source=="2"] icon="none"
|
||||
|
||||
Text item=nuvo_s3_display_line1 visibility=[nuvo_z1_source=="3"] icon="zoom"
|
||||
Text item=nuvo_s3_display_line2 visibility=[nuvo_z1_source=="3"] icon="zoom"
|
||||
Text item=nuvo_s3_display_line3 visibility=[nuvo_z1_source=="3"] icon="zoom"
|
||||
Text item=nuvo_s3_display_line4 visibility=[nuvo_z1_source=="3"] icon="zoom"
|
||||
Text item=nuvo_s3_play_mode visibility=[nuvo_z1_source=="3"] icon="player"
|
||||
Text item=nuvo_s3_track_length visibility=[nuvo_z1_source=="3"]
|
||||
Text item=nuvo_s3_track_position visibility=[nuvo_z1_source=="3"]
|
||||
Text item=nuvo_s3_button_press visibility=[nuvo_z1_source=="3"] icon="none"
|
||||
|
||||
Text item=nuvo_s4_display_line1 visibility=[nuvo_z1_source=="4"] icon="zoom"
|
||||
Text item=nuvo_s4_display_line2 visibility=[nuvo_z1_source=="4"] icon="zoom"
|
||||
Text item=nuvo_s4_display_line3 visibility=[nuvo_z1_source=="4"] icon="zoom"
|
||||
Text item=nuvo_s4_display_line4 visibility=[nuvo_z1_source=="4"] icon="zoom"
|
||||
Text item=nuvo_s4_play_mode visibility=[nuvo_z1_source=="4"] icon="player"
|
||||
Text item=nuvo_s4_track_length visibility=[nuvo_z1_source=="4"]
|
||||
Text item=nuvo_s4_track_position visibility=[nuvo_z1_source=="4"]
|
||||
Text item=nuvo_s4_button_press visibility=[nuvo_z1_source=="4"] icon="none"
|
||||
|
||||
Text item=nuvo_s5_display_line1 visibility=[nuvo_z1_source=="5"] icon="zoom"
|
||||
Text item=nuvo_s5_display_line2 visibility=[nuvo_z1_source=="5"] icon="zoom"
|
||||
Text item=nuvo_s5_display_line3 visibility=[nuvo_z1_source=="5"] icon="zoom"
|
||||
Text item=nuvo_s5_display_line4 visibility=[nuvo_z1_source=="5"] icon="zoom"
|
||||
Text item=nuvo_s5_play_mode visibility=[nuvo_z1_source=="5"] icon="player"
|
||||
Text item=nuvo_s5_track_length visibility=[nuvo_z1_source=="5"]
|
||||
Text item=nuvo_s5_track_position visibility=[nuvo_z1_source=="5"]
|
||||
Text item=nuvo_s5_button_press visibility=[nuvo_z1_source=="5"] icon="none"
|
||||
|
||||
Text item=nuvo_s6_display_line1 visibility=[nuvo_z1_source=="6"] icon="zoom"
|
||||
Text item=nuvo_s6_display_line2 visibility=[nuvo_z1_source=="6"] icon="zoom"
|
||||
Text item=nuvo_s6_display_line3 visibility=[nuvo_z1_source=="6"] icon="zoom"
|
||||
Text item=nuvo_s6_display_line4 visibility=[nuvo_z1_source=="6"] icon="zoom"
|
||||
Text item=nuvo_s6_play_mode visibility=[nuvo_z1_source=="6"] icon="player"
|
||||
Text item=nuvo_s6_track_length visibility=[nuvo_z1_source=="6"]
|
||||
Text item=nuvo_s6_track_position visibility=[nuvo_z1_source=="6"]
|
||||
Text item=nuvo_s6_button_press visibility=[nuvo_z1_source=="6"] icon="none"
|
||||
|
||||
Setpoint item=nuvo_z1_treble label="Treble Adjustment [%d]" minValue=-18 maxValue=18 step=2 visibility=[nuvo_z1_power==ON]
|
||||
Setpoint item=nuvo_z1_bass label="Bass Adjustment [%d]" minValue=-18 maxValue=18 step=2 visibility=[nuvo_z1_power==ON]
|
||||
Setpoint item=nuvo_z1_balance label="Balance Adjustment [%d]" minValue=-18 maxValue=18 step=2 visibility=[nuvo_z1_power==ON]
|
||||
Switch item=nuvo_z1_loudness visibility=[nuvo_z1_power==ON]
|
||||
Switch item=nuvo_z1_dnd visibility=[nuvo_z1_power==ON]
|
||||
Text item=nuvo_z1_lock label="Zone Locked: [%s]" icon="lock"
|
||||
Switch item=nuvo_z1_party visibility=[nuvo_z1_power==ON]
|
||||
}
|
||||
|
||||
//repeat for zones 2-20 (substitute z1)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
nuvo.rules:
|
||||
|
||||
```java
|
||||
import java.text.Normalizer
|
||||
|
||||
val actions = getActions("nuvo","nuvo:amplifier:myamp")
|
||||
|
||||
// send command a custom command to the Nuvo Amplifier
|
||||
// see 'NuVo Grand Concerto Serial Control Protocol.pdf' for more command examples
|
||||
// https://www.legrand.us/-/media/brands/nuvo/nuvo/catalog/softwaredownloads-new/i8g_e6g_control_protocol.ashx
|
||||
// commands send through the binding do not need the leading '*'
|
||||
|
||||
rule "Nuvo Custom Command example"
|
||||
when
|
||||
Item SomeItemTrigger received command
|
||||
then
|
||||
if(null === actions) {
|
||||
logInfo("actions", "Actions not found, check thing ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Send a message to Source 3
|
||||
//actions.sendNuvoCommand("S3MSG\"Hello World\",0,0")
|
||||
|
||||
// Send a message to Zone 11
|
||||
//actions.sendNuvoCommand("Z11MSG\"Hello World\",0,0")
|
||||
|
||||
// Select a Favorite (1-12) for Zone 2
|
||||
//actions.sendNuvoCommand("Z2FAV1")
|
||||
|
||||
end
|
||||
|
||||
// In the below examples, a method for maintaing Metadata information
|
||||
// for a hypothetical non NuvoNet Source 3 is demonstrated
|
||||
|
||||
// Item_Containing_TrackLength should get a 'received update' when the track changes
|
||||
// ('changed' is not sufficient if two consecutive tracks are the same length)
|
||||
|
||||
rule "Load track play info for Source 3"
|
||||
when
|
||||
Item Item_Containing_TrackLength received update
|
||||
then
|
||||
if(null === actions) {
|
||||
logInfo("actions", "Actions not found, check thing ID")
|
||||
return
|
||||
}
|
||||
// strip off any non-numeric characters and multiply seconds by 10 (Nuvo expects tenths of a second)
|
||||
var int trackLength = Integer::parseInt(Item_Containing_TrackLength.state.toString.replaceAll("[\\D]", "")) * 10
|
||||
|
||||
// '0' indicates the track is just starting (at position 0), '2' indicates to Nuvo that the track is playing
|
||||
// The Nuvo keypad will now begin counting up the elapsed time displayed (starting from 0)
|
||||
actions.sendNuvoCommand("S3DISPINFO," + trackLength.toString() + ",0,2")
|
||||
|
||||
end
|
||||
|
||||
rule "Load track name for Source 3"
|
||||
when
|
||||
Item Item_Containing_TrackName changed
|
||||
then
|
||||
// The Nuvo keypad cannot display extended ASCII characters (accent, umulat, etc.)
|
||||
// Below we transform extended ASCII chars into their basic counterparts
|
||||
// example: 'La Touché' becomes 'La Touche' and 'Nöel' becomes 'Noel'
|
||||
var trackName = Normalizer::normalize(Item_Containing_TrackName.state.toString, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "")
|
||||
|
||||
nuvo_s3_display_line4.sendCommand(trackName)
|
||||
nuvo_s3_display_line1.sendCommand("")
|
||||
|
||||
end
|
||||
|
||||
rule "Load album name for Source 3"
|
||||
when
|
||||
Item Item_Containing_AlbumName changed
|
||||
then
|
||||
// fix extended ASCII chars
|
||||
var albumName = Normalizer::normalize(Item_Containing_AlbumName.state.toString, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "")
|
||||
|
||||
nuvo_s3_display_line2.sendCommand(albumName)
|
||||
end
|
||||
|
||||
rule "Load artist name for Source 3"
|
||||
when
|
||||
Item Item_Containing_ArtistName changed
|
||||
then
|
||||
// fix extended ASCII chars
|
||||
var artistName = Normalizer::normalize(Item_Containing_ArtistName.state.toString, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "")
|
||||
|
||||
nuvo_s3_display_line3.sendCommand(artistName)
|
||||
end
|
||||
|
||||
// In this rule we have three items: Item_Containing_PlayMode, Item_Containing_TrackLength & Item_Containing_TrackPosition
|
||||
// Item_Containing_PlayMode reports the playing state of the music source as a string such as 'Playing' or 'Paused'
|
||||
// Item_Containing_TrackLength reports the length of the track in seconds
|
||||
// Item_Containing_TrackPosition report the current playback position of the track in seconds
|
||||
|
||||
rule "Update play state info for Source 3"
|
||||
when
|
||||
Item Item_Containing_PlayMode changed
|
||||
then
|
||||
var playMode = Item_Containing_PlayMode.state.toString()
|
||||
|
||||
// strip off any non-numeric characters and multiply seconds by 10 (Nuvo expects tenths of a second)
|
||||
var int trackLength = Integer::parseInt(Item_Containing_TrackLength.state.toString.replaceAll("[\\D]", "")) * 10
|
||||
var int trackPosition = Integer::parseInt(Item_Containing_TrackPosition.state.toString.replaceAll("[\\D]", "")) * 10
|
||||
|
||||
if(null === actions) {
|
||||
logInfo("actions", "Actions not found, check thing ID")
|
||||
return
|
||||
}
|
||||
|
||||
switch playMode {
|
||||
case "Nothing playing": {
|
||||
// when idle, '1' tells Nuvo to display 'idle' on the keypad
|
||||
actions.sendNuvoCommand("S3DISPINFO,0,0,1")
|
||||
}
|
||||
case "Playing": {
|
||||
// when playback starts or resumes, '2' tells Nuvo to display 'playing' on the keypad
|
||||
// trackPosition does not need to be updated continuously, Nuvo will automatically count up the elapsed time displayed on the keypad
|
||||
actions.sendNuvoCommand("S3DISPINFO," + trackLength.toString() + "," + trackPosition.toString() + ",2")
|
||||
}
|
||||
case "Paused": {
|
||||
// when playback is paused, '3' tells Nuvo to display 'paused' on the keypad and stop counting up the elapsed time
|
||||
// trackPosition should indicate the time elapsed of the track when playback was paused
|
||||
actions.sendNuvoCommand("S3DISPINFO," + trackLength.toString() + "," + trackPosition.toString() + ",3")
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
```
|
||||
BIN
bundles/org.openhab.binding.nuvo/doc/nuvo_logo.png
Normal file
BIN
bundles/org.openhab.binding.nuvo/doc/nuvo_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
17
bundles/org.openhab.binding.nuvo/pom.xml
Normal file
17
bundles/org.openhab.binding.nuvo/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.nuvo</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Nuvo Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.nuvo-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-nuvo" description="Nuvo Whole House Audio Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-serial</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.nuvo/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link INuvoThingActions} defines the interface for all thing actions supported by the binding.
|
||||
* These methods, parameters, and return types are explained in {@link NuvoThingActions}.
|
||||
*
|
||||
* @author Michael Lobstein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface INuvoThingActions {
|
||||
|
||||
void sendNuvoCommand(String rawCommand);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link NuvoBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Michael Lobstein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NuvoBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "nuvo";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_AMP = new ThingTypeUID(BINDING_ID, "amplifier");
|
||||
|
||||
// List of all Channel types
|
||||
// system
|
||||
public static final String CHANNEL_TYPE_ALLOFF = "alloff";
|
||||
public static final String CHANNEL_TYPE_ALLMUTE = "allmute";
|
||||
public static final String CHANNEL_TYPE_PAGE = "page";
|
||||
public static final String CHANNEL_TYPE_SENDCMD = "sendcmd";
|
||||
|
||||
// zone
|
||||
public static final String CHANNEL_TYPE_POWER = "power";
|
||||
public static final String CHANNEL_TYPE_SOURCE = "source";
|
||||
public static final String CHANNEL_TYPE_VOLUME = "volume";
|
||||
public static final String CHANNEL_TYPE_MUTE = "mute";
|
||||
public static final String CHANNEL_TYPE_CONTROL = "control";
|
||||
public static final String CHANNEL_TYPE_TREBLE = "treble";
|
||||
public static final String CHANNEL_TYPE_BASS = "bass";
|
||||
public static final String CHANNEL_TYPE_BALANCE = "balance";
|
||||
public static final String CHANNEL_TYPE_LOUDNESS = "loudness";
|
||||
public static final String CHANNEL_TYPE_DND = "dnd";
|
||||
public static final String CHANNEL_TYPE_LOCK = "lock";
|
||||
public static final String CHANNEL_TYPE_PARTY = "party";
|
||||
|
||||
// source
|
||||
public static final String CHANNEL_DISPLAY_LINE = "display_line";
|
||||
public static final String CHANNEL_DISPLAY_LINE1 = "display_line1";
|
||||
public static final String CHANNEL_DISPLAY_LINE2 = "display_line2";
|
||||
public static final String CHANNEL_DISPLAY_LINE3 = "display_line3";
|
||||
public static final String CHANNEL_DISPLAY_LINE4 = "display_line4";
|
||||
public static final String CHANNEL_PLAY_MODE = "play_mode";
|
||||
public static final String CHANNEL_TRACK_LENGTH = "track_length";
|
||||
public static final String CHANNEL_TRACK_POSITION = "track_position";
|
||||
public static final String CHANNEL_BUTTON_PRESS = "button_press";
|
||||
|
||||
// Message types
|
||||
public static final String TYPE_VERSION = "version";
|
||||
public static final String TYPE_ALLOFF = "alloff";
|
||||
public static final String TYPE_ALLMUTE = "allmute";
|
||||
public static final String TYPE_PAGE = "page";
|
||||
public static final String TYPE_SOURCE_UPDATE = "source_update";
|
||||
public static final String TYPE_ZONE_UPDATE = "zone_update";
|
||||
public static final String TYPE_ZONE_BUTTON = "zone_button";
|
||||
public static final String TYPE_ZONE_CONFIG = "zone_config";
|
||||
|
||||
// misc
|
||||
public static final String ON = "ON";
|
||||
public static final String OFF = "OFF";
|
||||
public static final String ONE = "1";
|
||||
public static final String ZERO = "0";
|
||||
public static final String BLANK = "";
|
||||
public static final String DISPLINE = "DISPLINE";
|
||||
public static final String DISPINFO = "DISPINFO,"; // yes comma here
|
||||
public static final String NAME_QUOTE = "NAME\"";
|
||||
public static final String MUTE = "MUTE";
|
||||
public static final String VOL = "VOL";
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link NuvoException} class is used for any exception thrown by the binding
|
||||
*
|
||||
* @author Michael Lobstein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NuvoException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public NuvoException() {
|
||||
}
|
||||
|
||||
public NuvoException(String message, Throwable t) {
|
||||
super(message, t);
|
||||
}
|
||||
|
||||
public NuvoException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal;
|
||||
|
||||
import static org.openhab.binding.nuvo.internal.NuvoBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nuvo.internal.handler.NuvoHandler;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link NuvoHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Michael Lobstein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.nuvo", service = ThingHandlerFactory.class)
|
||||
public class NuvoHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_AMP);
|
||||
|
||||
private final SerialPortManager serialPortManager;
|
||||
|
||||
private final NuvoStateDescriptionOptionProvider stateDescriptionProvider;
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Activate
|
||||
public NuvoHandlerFactory(final @Reference NuvoStateDescriptionOptionProvider provider,
|
||||
final @Reference SerialPortManager serialPortManager) {
|
||||
this.stateDescriptionProvider = provider;
|
||||
this.serialPortManager = serialPortManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
|
||||
return new NuvoHandler(thing, stateDescriptionProvider, serialPortManager);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
|
||||
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
|
||||
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* Dynamic provider of state options while leaving other state description fields as original.
|
||||
*
|
||||
* @author Michael Lobstein - Initial contribution
|
||||
*/
|
||||
@Component(service = { DynamicStateDescriptionProvider.class, NuvoStateDescriptionOptionProvider.class })
|
||||
@NonNullByDefault
|
||||
public class NuvoStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider {
|
||||
|
||||
@Reference
|
||||
protected void setChannelTypeI18nLocalizationService(
|
||||
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
|
||||
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
|
||||
}
|
||||
|
||||
protected void unsetChannelTypeI18nLocalizationService(
|
||||
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
|
||||
this.channelTypeI18nLocalizationService = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nuvo.internal.handler.NuvoHandler;
|
||||
import org.openhab.core.automation.annotation.ActionInput;
|
||||
import org.openhab.core.automation.annotation.RuleAction;
|
||||
import org.openhab.core.thing.binding.ThingActions;
|
||||
import org.openhab.core.thing.binding.ThingActionsScope;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Some automation actions to be used with a {@link NuvoThingActions}
|
||||
*
|
||||
* @author Michael Lobstein - initial contribution
|
||||
*
|
||||
*/
|
||||
@ThingActionsScope(name = "nuvo")
|
||||
@NonNullByDefault
|
||||
public class NuvoThingActions implements ThingActions, INuvoThingActions {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NuvoThingActions.class);
|
||||
|
||||
private @Nullable NuvoHandler handler;
|
||||
|
||||
@RuleAction(label = "sendNuvoCommand", description = "Action that sends raw command to the amplifer")
|
||||
public void sendNuvoCommand(@ActionInput(name = "sendNuvoCommand") String rawCommand) {
|
||||
NuvoHandler localHandler = handler;
|
||||
if (localHandler != null) {
|
||||
localHandler.handleRawCommand(rawCommand);
|
||||
logger.debug("sendNuvoCommand called with raw command: {}", rawCommand);
|
||||
} else {
|
||||
logger.warn("unable to send command, NuvoHandler was null");
|
||||
}
|
||||
}
|
||||
|
||||
/** Static alias to support the old DSL rules engine and make the action available there. */
|
||||
public static void sendNuvoCommand(@Nullable ThingActions actions, String rawCommand)
|
||||
throws IllegalArgumentException {
|
||||
invokeMethodOf(actions).sendNuvoCommand(rawCommand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
this.handler = (NuvoHandler) handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return this.handler;
|
||||
}
|
||||
|
||||
private static INuvoThingActions invokeMethodOf(@Nullable ThingActions actions) {
|
||||
if (actions == null) {
|
||||
throw new IllegalArgumentException("actions cannot be null");
|
||||
}
|
||||
if (actions.getClass().getName().equals(NuvoThingActions.class.getName())) {
|
||||
if (actions instanceof NuvoThingActions) {
|
||||
return (INuvoThingActions) actions;
|
||||
} else {
|
||||
return (INuvoThingActions) Proxy.newProxyInstance(INuvoThingActions.class.getClassLoader(),
|
||||
new Class[] { INuvoThingActions.class }, (Object proxy, Method method, Object[] args) -> {
|
||||
Method m = actions.getClass().getDeclaredMethod(method.getName(),
|
||||
method.getParameterTypes());
|
||||
return m.invoke(actions, args);
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Actions is not an instance of NuvoThingActions");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal.communication;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Represents the different kinds of commands
|
||||
*
|
||||
* @author Michael Lobstein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum NuvoCommand {
|
||||
GET_CONTROLLER_VERSION("VER"),
|
||||
ALLMUTE_ON("MUTE1"),
|
||||
ALLMUTE_OFF("MUTE0"),
|
||||
ALLOFF("ALLOFF"),
|
||||
PAGE_ON("PAGE1"),
|
||||
PAGE_OFF("PAGE0"),
|
||||
CFGTIME("CFGTIME"),
|
||||
STATUS("STATUS"),
|
||||
EQ_QUERY("EQ?"),
|
||||
DISPINFO("DISPINFO"),
|
||||
DISPLINE("DISPLINE"),
|
||||
DISPLINE1("DISPLINE1"),
|
||||
DISPLINE2("DISPLINE2"),
|
||||
DISPLINE3("DISPLINE3"),
|
||||
DISPLINE4("DISPLINE4"),
|
||||
NAME("NAME"),
|
||||
ON("ON"),
|
||||
OFF("OFF"),
|
||||
SOURCE("SRC"),
|
||||
VOLUME("VOL"),
|
||||
MUTE_ON("MUTEON"),
|
||||
MUTE_OFF("MUTEOFF"),
|
||||
TREBLE("TREB"),
|
||||
BASS("BASS"),
|
||||
BALANCE("BAL"),
|
||||
LOUDNESS("LOUDCMP"),
|
||||
PLAYPAUSE("PLAYPAUSE"),
|
||||
PREV("PREV"),
|
||||
NEXT("NEXT"),
|
||||
DND_ON("DNDON"),
|
||||
DND_OFF("DNDOFF"),
|
||||
PARTY_ON("PARTY1"),
|
||||
PARTY_OFF("PARTY0");
|
||||
|
||||
private final String value;
|
||||
|
||||
NuvoCommand(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command name
|
||||
*
|
||||
* @return the command name
|
||||
*/
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal.communication;
|
||||
|
||||
import static org.openhab.binding.nuvo.internal.NuvoBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nuvo.internal.NuvoException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Abstract class for communicating with the Nuvo device
|
||||
*
|
||||
* @author Laurent Garnier - Initial contribution
|
||||
* @author Michael Lobstein - Adapted for the Nuvo binding
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class NuvoConnector {
|
||||
private static final String COMMAND_OK = "#OK";
|
||||
private static final String BEGIN_CMD = "*";
|
||||
private static final String END_CMD = "\r";
|
||||
private static final String QUERY = "?";
|
||||
private static final String VER_STR = "#VER\"NV-";
|
||||
private static final String ALL_OFF = "#ALLOFF";
|
||||
private static final String MUTE = "#MUTE";
|
||||
private static final String PAGE = "#PAGE";
|
||||
|
||||
private static final byte[] WAKE_STR = "\r".getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
private static final Pattern SRC_PATTERN = Pattern.compile("^#S(\\d{1})(.*)$");
|
||||
private static final Pattern ZONE_PATTERN = Pattern.compile("^#Z(\\d{1,2}),(.*)$");
|
||||
private static final Pattern ZONE_BUTTON_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})(.*)$");
|
||||
private static final Pattern ZONE_CFG_PATTERN = Pattern.compile("^#ZCFG(\\d{1,2}),(.*)$");
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NuvoConnector.class);
|
||||
|
||||
protected static final String COMMAND_ERROR = "#?";
|
||||
|
||||
/** The output stream */
|
||||
protected @Nullable OutputStream dataOut;
|
||||
|
||||
/** The input stream */
|
||||
protected @Nullable InputStream dataIn;
|
||||
|
||||
/** true if the connection is established, false if not */
|
||||
private boolean connected;
|
||||
|
||||
private @Nullable Thread readerThread;
|
||||
|
||||
private List<NuvoMessageEventListener> listeners = new ArrayList<>();
|
||||
|
||||
private boolean isEssentia = true;
|
||||
|
||||
/**
|
||||
* Get whether the connection is established or not
|
||||
*
|
||||
* @return true if the connection is established
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
return connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the connection is established or not
|
||||
*
|
||||
* @param connected true if the connection is established
|
||||
*/
|
||||
protected void setConnected(boolean connected) {
|
||||
this.connected = connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the connector if the device is an Essentia G or not
|
||||
*
|
||||
* @param true if the device is an Essentia G
|
||||
*/
|
||||
public void setEssentia(boolean isEssentia) {
|
||||
this.isEssentia = isEssentia;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the thread that handles the feedback messages
|
||||
*
|
||||
* @param readerThread the thread
|
||||
*/
|
||||
protected void setReaderThread(Thread readerThread) {
|
||||
this.readerThread = readerThread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the connection with the Nuvo device
|
||||
*
|
||||
* @throws NuvoException - In case of any problem
|
||||
*/
|
||||
public abstract void open() throws NuvoException;
|
||||
|
||||
/**
|
||||
* Close the connection with the Nuvo device
|
||||
*/
|
||||
public abstract void close();
|
||||
|
||||
/**
|
||||
* Stop the thread that handles the feedback messages and close the opened input and output streams
|
||||
*/
|
||||
protected void cleanup() {
|
||||
Thread readerThread = this.readerThread;
|
||||
OutputStream dataOut = this.dataOut;
|
||||
if (dataOut != null) {
|
||||
try {
|
||||
dataOut.close();
|
||||
} catch (IOException e) {
|
||||
logger.debug("Error closing dataOut: {}", e.getMessage());
|
||||
}
|
||||
this.dataOut = null;
|
||||
}
|
||||
InputStream dataIn = this.dataIn;
|
||||
if (dataIn != null) {
|
||||
try {
|
||||
dataIn.close();
|
||||
} catch (IOException e) {
|
||||
logger.debug("Error closing dataIn: {}", e.getMessage());
|
||||
}
|
||||
this.dataIn = null;
|
||||
}
|
||||
if (readerThread != null) {
|
||||
readerThread.interrupt();
|
||||
this.readerThread = null;
|
||||
try {
|
||||
readerThread.join(3000);
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Error joining readerThread: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
|
||||
* actually read is returned as an integer.
|
||||
*
|
||||
* @param dataBuffer the buffer into which the data is read.
|
||||
*
|
||||
* @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
|
||||
* stream has been reached.
|
||||
*
|
||||
* @throws NuvoException - If the input stream is null, if the first byte cannot be read for any reason
|
||||
* other than the end of the file, if the input stream has been closed, or if some other I/O error
|
||||
* occurs.
|
||||
*/
|
||||
protected int readInput(byte[] dataBuffer) throws NuvoException {
|
||||
InputStream dataIn = this.dataIn;
|
||||
if (dataIn == null) {
|
||||
throw new NuvoException("readInput failed: input stream is null");
|
||||
}
|
||||
try {
|
||||
return dataIn.read(dataBuffer);
|
||||
} catch (IOException e) {
|
||||
throw new NuvoException("readInput failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the Nuvo controller to execute an inquiry command
|
||||
*
|
||||
* @param zone the zone for which the command is to be run
|
||||
* @param cmd the command to execute
|
||||
*
|
||||
* @throws NuvoException - In case of any problem
|
||||
*/
|
||||
public void sendQuery(NuvoEnum zone, NuvoCommand cmd) throws NuvoException {
|
||||
sendCommand(zone.getId() + cmd.getValue() + QUERY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the Nuvo controller to execute a command for a zone that takes no arguments (ie power on, power off,
|
||||
* etc.)
|
||||
*
|
||||
* @param zone the zone for which the command is to be run
|
||||
* @param cmd the command to execute
|
||||
*
|
||||
* @throws NuvoException - In case of any problem
|
||||
*/
|
||||
public void sendCommand(NuvoEnum zone, NuvoCommand cmd) throws NuvoException {
|
||||
sendCommand(zone.getId() + cmd.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the Nuvo controller to execute a command for a zone and pass in a value
|
||||
*
|
||||
* @param zone the zone for which the command is to be run
|
||||
* @param cmd the command to execute
|
||||
* @param value the string value to consider for volume, source, etc.
|
||||
*
|
||||
* @throws NuvoException - In case of any problem
|
||||
*/
|
||||
public void sendCommand(NuvoEnum zone, NuvoCommand cmd, @Nullable String value) throws NuvoException {
|
||||
sendCommand(zone.getId() + cmd.getValue() + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the Nuvo controller to execute a configuration command for a zone and pass in a value
|
||||
*
|
||||
* @param zone the zone for which the command is to be run
|
||||
* @param cmd the command to execute
|
||||
* @param value the string value to consider for bass, treble, balance, etc.
|
||||
*
|
||||
* @throws NuvoException - In case of any problem
|
||||
*/
|
||||
public void sendCfgCommand(NuvoEnum zone, NuvoCommand cmd, @Nullable String value) throws NuvoException {
|
||||
sendCommand(zone.getConfigId() + cmd.getValue() + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the Nuvo controller to execute a system command the does not specify a zone or value
|
||||
*
|
||||
* @param cmd the command to execute
|
||||
*
|
||||
* @throws NuvoException - In case of any problem
|
||||
*/
|
||||
public void sendCommand(NuvoCommand cmd) throws NuvoException {
|
||||
sendCommand(cmd.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the Nuvo controller to execute a raw command string
|
||||
*
|
||||
* @param command the command string to run
|
||||
*
|
||||
* @throws NuvoException - In case of any problem
|
||||
*/
|
||||
public void sendCommand(@Nullable String command) throws NuvoException {
|
||||
String messageStr = BEGIN_CMD + command + END_CMD;
|
||||
|
||||
logger.debug("sending command: {}", messageStr);
|
||||
|
||||
OutputStream dataOut = this.dataOut;
|
||||
if (dataOut == null) {
|
||||
throw new NuvoException("Send command \"" + messageStr + "\" failed: output stream is null");
|
||||
}
|
||||
try {
|
||||
// Essentia G needs time to wake up when in standby mode
|
||||
// I don't want to track that in the binding, so just do this always
|
||||
if (this.isEssentia) {
|
||||
dataOut.write(WAKE_STR);
|
||||
dataOut.flush();
|
||||
}
|
||||
dataOut.write(messageStr.getBytes(StandardCharsets.US_ASCII));
|
||||
dataOut.flush();
|
||||
} catch (IOException e) {
|
||||
throw new NuvoException("Send command \"" + command + "\" failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to the list of listeners to be notified with events
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void addEventListener(NuvoMessageEventListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a listener from the list of listeners to be notified with events
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void removeEventListener(NuvoMessageEventListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze an incoming message and dispatch corresponding (type, key, value) to the event listeners
|
||||
*
|
||||
* @param incomingMessage the received message
|
||||
*/
|
||||
public void handleIncomingMessage(byte[] incomingMessage) {
|
||||
String message = new String(incomingMessage, StandardCharsets.US_ASCII).trim();
|
||||
|
||||
logger.debug("handleIncomingMessage: {}", message);
|
||||
|
||||
if (COMMAND_ERROR.equals(message) || COMMAND_OK.equals(message)) {
|
||||
// ignore
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.contains(VER_STR)) {
|
||||
// example: #VER"NV-E6G FWv2.66 HWv0"
|
||||
// split on " and return the version number
|
||||
dispatchKeyValue(TYPE_VERSION, "", message.split("\"")[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.equals(ALL_OFF)) {
|
||||
dispatchKeyValue(TYPE_ALLOFF, BLANK, BLANK);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.contains(MUTE)) {
|
||||
dispatchKeyValue(TYPE_ALLMUTE, BLANK, message.substring(message.length() - 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.contains(PAGE)) {
|
||||
dispatchKeyValue(TYPE_PAGE, BLANK, message.substring(message.length() - 1));
|
||||
return;
|
||||
}
|
||||
|
||||
// Amp controller send a source update ie: #S2DISPINFO,DUR3380,POS3090,STATUS2
|
||||
// or #S2DISPLINE1,"1 of 17"
|
||||
Matcher matcher = SRC_PATTERN.matcher(message);
|
||||
if (matcher.find()) {
|
||||
// pull out the source id and the remainder of the message
|
||||
dispatchKeyValue(TYPE_SOURCE_UPDATE, matcher.group(1), matcher.group(2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Amp controller send a zone update ie: #Z11,ON,SRC3,VOL63,DND0,LOCK0
|
||||
matcher = ZONE_PATTERN.matcher(message);
|
||||
if (matcher.find()) {
|
||||
// pull out the zone id and the remainder of the message
|
||||
dispatchKeyValue(TYPE_ZONE_UPDATE, matcher.group(1), matcher.group(2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Amp controller send a zone button press event ie: #Z11S3PLAYPAUSE
|
||||
matcher = ZONE_BUTTON_PATTERN.matcher(message);
|
||||
if (matcher.find()) {
|
||||
// pull out the source id and the remainder of the message, ignore the zone id
|
||||
dispatchKeyValue(TYPE_ZONE_BUTTON, matcher.group(2), matcher.group(3));
|
||||
return;
|
||||
}
|
||||
|
||||
// Amp controller send a zone configuration response ie: #ZCFG1,BASS1,TREB-2,BALR2,LOUDCMP1
|
||||
matcher = ZONE_CFG_PATTERN.matcher(message);
|
||||
if (matcher.find()) {
|
||||
// pull out the zone id and the remainder of the message
|
||||
dispatchKeyValue(TYPE_ZONE_CONFIG, matcher.group(1), matcher.group(2));
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("unhandled message: {}", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch an event (type, key, value) to the event listeners
|
||||
*
|
||||
* @param type the type
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
*/
|
||||
private void dispatchKeyValue(String type, String key, String value) {
|
||||
NuvoMessageEvent event = new NuvoMessageEvent(this, type, key, value);
|
||||
listeners.forEach(l -> l.onNewMessageEvent(event));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal.communication;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.nuvo.internal.NuvoException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Class to create a default NuvoDefaultConnector before initialization is complete.
|
||||
*
|
||||
* @author Laurent Garnier - Initial contribution
|
||||
* @author Michael Lobstein - Adapted for the Nuvo binding
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NuvoDefaultConnector extends NuvoConnector {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NuvoDefaultConnector.class);
|
||||
|
||||
@Override
|
||||
public void open() throws NuvoException {
|
||||
logger.warn("Nuvo binding incorrectly configured. Please configure for Serial or IP over serial connection");
|
||||
setConnected(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
setConnected(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal.communication;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Represents the different internal zone and source IDs of the Nuvo Whole House Amplifier
|
||||
*
|
||||
* @author Michael Lobstein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum NuvoEnum {
|
||||
SYSTEM("SYSTEM", "SYSTEM"),
|
||||
ZONE1("Z1", "ZCFG1"),
|
||||
ZONE2("Z2", "ZCFG2"),
|
||||
ZONE3("Z3", "ZCFG3"),
|
||||
ZONE4("Z4", "ZCFG4"),
|
||||
ZONE5("Z5", "ZCFG5"),
|
||||
ZONE6("Z6", "ZCFG6"),
|
||||
ZONE7("Z7", "ZCFG7"),
|
||||
ZONE8("Z8", "ZCFG8"),
|
||||
ZONE9("Z9", "ZCFG9"),
|
||||
ZONE10("Z10", "ZCFG10"),
|
||||
ZONE11("Z11", "ZCFG11"),
|
||||
ZONE12("Z12", "ZCFG12"),
|
||||
ZONE13("Z13", "ZCFG13"),
|
||||
ZONE14("Z14", "ZCFG14"),
|
||||
ZONE15("Z15", "ZCFG15"),
|
||||
ZONE16("Z16", "ZCFG16"),
|
||||
ZONE17("Z17", "ZCFG17"),
|
||||
ZONE18("Z18", "ZCFG18"),
|
||||
ZONE19("Z19", "ZCFG19"),
|
||||
ZONE20("Z20", "ZCFG20"),
|
||||
SOURCE1("S1", "SCFG1"),
|
||||
SOURCE2("S2", "SCFG2"),
|
||||
SOURCE3("S3", "SCFG3"),
|
||||
SOURCE4("S4", "SCFG4"),
|
||||
SOURCE5("S5", "SCFG5"),
|
||||
SOURCE6("S6", "SCFG6");
|
||||
|
||||
private final String id;
|
||||
private final String cfgId;
|
||||
|
||||
// make a list of all valid source ids
|
||||
public static final List<String> VALID_SOURCES = Arrays.stream(values()).map(NuvoEnum::name)
|
||||
.filter(s -> s.contains("SOURCE")).collect(Collectors.toList());
|
||||
|
||||
NuvoEnum(String id, String cfgId) {
|
||||
this.id = id;
|
||||
this.cfgId = cfgId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the id
|
||||
*
|
||||
* @return the id
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config id
|
||||
*
|
||||
* @return the config id
|
||||
*/
|
||||
public String getConfigId() {
|
||||
return cfgId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal.communication;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketTimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nuvo.internal.NuvoException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Class for communicating with the Nuvo device through a serial over IP connection
|
||||
*
|
||||
* @author Laurent Garnier - Initial contribution
|
||||
* @author Michael Lobstein - Adapted for the Nuvo binding
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NuvoIpConnector extends NuvoConnector {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NuvoIpConnector.class);
|
||||
|
||||
private @Nullable final String address;
|
||||
private final int port;
|
||||
private final String uid;
|
||||
|
||||
private @Nullable Socket clientSocket;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param address the IP address of the serial over ip adapter
|
||||
* @param port the TCP port to be used
|
||||
* @param uid the thing uid string
|
||||
*/
|
||||
public NuvoIpConnector(@Nullable String address, int port, String uid) {
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void open() throws NuvoException {
|
||||
logger.debug("Opening IP connection on IP {} port {}", this.address, this.port);
|
||||
try {
|
||||
Socket clientSocket = new Socket(this.address, this.port);
|
||||
clientSocket.setSoTimeout(100);
|
||||
|
||||
dataOut = new DataOutputStream(clientSocket.getOutputStream());
|
||||
dataIn = new DataInputStream(clientSocket.getInputStream());
|
||||
|
||||
Thread thread = new NuvoReaderThread(this, this.uid, this.address + "." + this.port);
|
||||
setReaderThread(thread);
|
||||
thread.start();
|
||||
|
||||
this.clientSocket = clientSocket;
|
||||
|
||||
setConnected(true);
|
||||
|
||||
logger.debug("IP connection opened");
|
||||
} catch (IOException | SecurityException | IllegalArgumentException e) {
|
||||
setConnected(false);
|
||||
throw new NuvoException("Opening IP connection failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
logger.debug("Closing IP connection");
|
||||
super.cleanup();
|
||||
Socket clientSocket = this.clientSocket;
|
||||
if (clientSocket != null) {
|
||||
try {
|
||||
clientSocket.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
this.clientSocket = null;
|
||||
}
|
||||
setConnected(false);
|
||||
logger.debug("IP connection closed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
|
||||
* actually read is returned as an integer.
|
||||
* In case of socket timeout, the returned value is 0.
|
||||
*
|
||||
* @param dataBuffer the buffer into which the data is read.
|
||||
*
|
||||
* @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
|
||||
* stream has been reached.
|
||||
*
|
||||
* @throws NuvoException - If the input stream is null, if the first byte cannot be read for any reason
|
||||
* other than the end of the file, if the input stream has been closed, or if some other I/O error
|
||||
* occurs.
|
||||
*/
|
||||
@Override
|
||||
protected int readInput(byte[] dataBuffer) throws NuvoException {
|
||||
InputStream dataIn = this.dataIn;
|
||||
if (dataIn == null) {
|
||||
throw new NuvoException("readInput failed: input stream is null");
|
||||
}
|
||||
try {
|
||||
return dataIn.read(dataBuffer);
|
||||
} catch (SocketTimeoutException e) {
|
||||
return 0;
|
||||
} catch (IOException e) {
|
||||
throw new NuvoException("readInput failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal.communication;
|
||||
|
||||
import java.util.EventObject;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* NuvoMessageEvent event used to notify changes coming from messages received from the Nuvo device
|
||||
*
|
||||
* @author Michael Lobstein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NuvoMessageEvent extends EventObject {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final String type;
|
||||
private final String key;
|
||||
private final String value;
|
||||
|
||||
public NuvoMessageEvent(Object source, String type, String key, String value) {
|
||||
super(source);
|
||||
this.type = type;
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal.communication;
|
||||
|
||||
import java.util.EventListener;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Nuvo Event Listener interface. Handles incoming Nuvo message events
|
||||
*
|
||||
* @author Michael Lobstein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface NuvoMessageEventListener extends EventListener {
|
||||
|
||||
/**
|
||||
* Event handler method for incoming Nuvo message events
|
||||
*
|
||||
* @param event the NuvoMessageEvent object
|
||||
*/
|
||||
public void onNewMessageEvent(NuvoMessageEvent event);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal.communication;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.nuvo.internal.NuvoException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A class that reads messages from the Nuvo device in a dedicated thread
|
||||
*
|
||||
* @author Laurent Garnier - Initial contribution
|
||||
* @author Michael Lobstein - Adapted for the Nuvo binding
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NuvoReaderThread extends Thread {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NuvoReaderThread.class);
|
||||
|
||||
private static final int READ_BUFFER_SIZE = 16;
|
||||
private static final int SIZE = 256;
|
||||
|
||||
private static final char TERM_CHAR = '\r';
|
||||
|
||||
private NuvoConnector connector;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param connector the object that should handle the received message
|
||||
* @param uid the thing uid string
|
||||
* @param connectionId a string that uniquely identifies the particular connection
|
||||
*/
|
||||
public NuvoReaderThread(NuvoConnector connector, String uid, String connectionId) {
|
||||
super("OH-binding-" + uid + "-" + connectionId);
|
||||
this.connector = connector;
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
logger.debug("Data listener started");
|
||||
|
||||
byte[] readDataBuffer = new byte[READ_BUFFER_SIZE];
|
||||
byte[] dataBuffer = new byte[SIZE];
|
||||
int index = 0;
|
||||
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
int len = connector.readInput(readDataBuffer);
|
||||
if (len > 0) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
|
||||
if (index < SIZE) {
|
||||
dataBuffer[index++] = readDataBuffer[i];
|
||||
}
|
||||
if (readDataBuffer[i] == TERM_CHAR) {
|
||||
if (index >= SIZE) {
|
||||
dataBuffer[index - 1] = (byte) TERM_CHAR;
|
||||
}
|
||||
byte[] msg = Arrays.copyOf(dataBuffer, index);
|
||||
connector.handleIncomingMessage(msg);
|
||||
index = 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (NuvoException e) {
|
||||
logger.debug("Reading failed: {}", e.getMessage(), e);
|
||||
connector.handleIncomingMessage(NuvoConnector.COMMAND_ERROR.getBytes());
|
||||
}
|
||||
|
||||
logger.debug("Data listener stopped");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal.communication;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nuvo.internal.NuvoException;
|
||||
import org.openhab.core.io.transport.serial.PortInUseException;
|
||||
import org.openhab.core.io.transport.serial.SerialPort;
|
||||
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Class for communicating with the Nuvo device through a serial connection
|
||||
*
|
||||
* @author Laurent Garnier - Initial contribution
|
||||
* @author Michael Lobstein - Adapted for the Nuvo binding
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NuvoSerialConnector extends NuvoConnector {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NuvoSerialConnector.class);
|
||||
|
||||
private final String serialPortName;
|
||||
private final SerialPortManager serialPortManager;
|
||||
private final String uid;
|
||||
|
||||
private @Nullable SerialPort serialPort;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param serialPortManager the serial port manager
|
||||
* @param serialPortName the serial port name to be used
|
||||
* @param uid the thing uid string
|
||||
*/
|
||||
public NuvoSerialConnector(SerialPortManager serialPortManager, String serialPortName, String uid) {
|
||||
this.serialPortManager = serialPortManager;
|
||||
this.serialPortName = serialPortName;
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void open() throws NuvoException {
|
||||
logger.debug("Opening serial connection on port {}", serialPortName);
|
||||
try {
|
||||
SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
|
||||
if (portIdentifier == null) {
|
||||
setConnected(false);
|
||||
logger.warn("Opening serial connection failed: No Such Port: {}", serialPortName);
|
||||
throw new NuvoException("Opening serial connection failed: No Such Port");
|
||||
}
|
||||
|
||||
SerialPort commPort = portIdentifier.open(this.getClass().getName(), 2000);
|
||||
|
||||
commPort.setSerialPortParams(57600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
|
||||
commPort.enableReceiveThreshold(1);
|
||||
commPort.enableReceiveTimeout(100);
|
||||
commPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
|
||||
|
||||
InputStream dataIn = commPort.getInputStream();
|
||||
OutputStream dataOut = commPort.getOutputStream();
|
||||
|
||||
if (dataOut != null) {
|
||||
dataOut.flush();
|
||||
}
|
||||
if (dataIn != null && dataIn.markSupported()) {
|
||||
try {
|
||||
dataIn.reset();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
|
||||
Thread thread = new NuvoReaderThread(this, this.uid, this.serialPortName);
|
||||
setReaderThread(thread);
|
||||
thread.start();
|
||||
|
||||
this.serialPort = commPort;
|
||||
this.dataIn = dataIn;
|
||||
this.dataOut = dataOut;
|
||||
|
||||
setConnected(true);
|
||||
|
||||
logger.debug("Serial connection opened");
|
||||
} catch (PortInUseException e) {
|
||||
setConnected(false);
|
||||
throw new NuvoException("Opening serial connection failed: Port in Use Exception", e);
|
||||
} catch (UnsupportedCommOperationException e) {
|
||||
setConnected(false);
|
||||
throw new NuvoException("Opening serial connection failed: Unsupported Comm Operation Exception", e);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
setConnected(false);
|
||||
throw new NuvoException("Opening serial connection failed: Unsupported Encoding Exception", e);
|
||||
} catch (IOException e) {
|
||||
setConnected(false);
|
||||
throw new NuvoException("Opening serial connection failed: IO Exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
logger.debug("Closing serial connection");
|
||||
SerialPort serialPort = this.serialPort;
|
||||
if (serialPort != null) {
|
||||
serialPort.removeEventListener();
|
||||
}
|
||||
super.cleanup();
|
||||
if (serialPort != null) {
|
||||
serialPort.close();
|
||||
this.serialPort = null;
|
||||
}
|
||||
setConnected(false);
|
||||
logger.debug("Serial connection closed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal.communication;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Provides mapping of various Nuvo status codes to plain language meanings
|
||||
*
|
||||
* @author Michael Lobstein - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class NuvoStatusCodes {
|
||||
private static final String L = "L";
|
||||
private static final String C = "C";
|
||||
private static final String R = "R";
|
||||
private static final String DASH = "-";
|
||||
private static final String ZERO = "0";
|
||||
|
||||
// map to lookup play mode
|
||||
public static final Map<String, String> PLAY_MODE = new HashMap<>();
|
||||
static {
|
||||
PLAY_MODE.put("0", "Normal");
|
||||
PLAY_MODE.put("1", "Idle");
|
||||
PLAY_MODE.put("2", "Playing");
|
||||
PLAY_MODE.put("3", "Paused");
|
||||
PLAY_MODE.put("4", "Fast Forward");
|
||||
PLAY_MODE.put("5", "Rewind");
|
||||
PLAY_MODE.put("6", "Play Shuffle");
|
||||
PLAY_MODE.put("7", "Play Repeat");
|
||||
PLAY_MODE.put("8", "Play Shuffle Repeat");
|
||||
PLAY_MODE.put("9", "unknown-9");
|
||||
PLAY_MODE.put("10", "unknown-10");
|
||||
PLAY_MODE.put("11", "Radio"); // undocumented
|
||||
PLAY_MODE.put("12", "unknown-12");
|
||||
}
|
||||
|
||||
/*
|
||||
* This looks broken because the controller is seriously broken...
|
||||
* On the keypad when adjusting the balance to "Left 18", the serial data reports R18 ¯\_(ツ)_/¯
|
||||
* So on top of the weird translation, the value needs to be reversed by the binding
|
||||
* to ensure that it will match what is displayed on the keypad.
|
||||
* For display purposes we want -18 to be full left, 0 = center, and +18 to be full right
|
||||
*/
|
||||
public static String getBalanceFromStr(String value) {
|
||||
// example L2; return 2 | C; return 0 | R10; return -10
|
||||
if (value.substring(0, 1).equals(L)) {
|
||||
return (value.substring(1));
|
||||
} else if (value.equals(C)) {
|
||||
return ZERO;
|
||||
} else if (value.substring(0, 1).equals(R)) {
|
||||
return (DASH + value.substring(1));
|
||||
}
|
||||
return ZERO;
|
||||
}
|
||||
|
||||
// see above comment
|
||||
public static String getBalanceFromInt(Integer value) {
|
||||
if (value < 0) {
|
||||
return (L + Math.abs(value));
|
||||
} else if (value == 0) {
|
||||
return C;
|
||||
} else if (value > 0) {
|
||||
return (R + value);
|
||||
}
|
||||
return C;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal.configuration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link NuvoThingConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Michael Lobstein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NuvoThingConfiguration {
|
||||
|
||||
public @Nullable String serialPort;
|
||||
public @Nullable String host;
|
||||
public @Nullable Integer port;
|
||||
public @Nullable Integer numZones;
|
||||
public boolean clockSync;
|
||||
}
|
||||
@@ -0,0 +1,793 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuvo.internal.handler;
|
||||
|
||||
import static org.openhab.binding.nuvo.internal.NuvoBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Time;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nuvo.internal.NuvoException;
|
||||
import org.openhab.binding.nuvo.internal.NuvoStateDescriptionOptionProvider;
|
||||
import org.openhab.binding.nuvo.internal.NuvoThingActions;
|
||||
import org.openhab.binding.nuvo.internal.communication.NuvoCommand;
|
||||
import org.openhab.binding.nuvo.internal.communication.NuvoConnector;
|
||||
import org.openhab.binding.nuvo.internal.communication.NuvoDefaultConnector;
|
||||
import org.openhab.binding.nuvo.internal.communication.NuvoEnum;
|
||||
import org.openhab.binding.nuvo.internal.communication.NuvoIpConnector;
|
||||
import org.openhab.binding.nuvo.internal.communication.NuvoMessageEvent;
|
||||
import org.openhab.binding.nuvo.internal.communication.NuvoMessageEventListener;
|
||||
import org.openhab.binding.nuvo.internal.communication.NuvoSerialConnector;
|
||||
import org.openhab.binding.nuvo.internal.communication.NuvoStatusCodes;
|
||||
import org.openhab.binding.nuvo.internal.configuration.NuvoThingConfiguration;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.NextPreviousType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.PlayPauseType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateOption;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link NuvoHandler} is responsible for handling commands, which are sent to one of the channels.
|
||||
*
|
||||
* Based on the Rotel binding by Laurent Garnier
|
||||
*
|
||||
* @author Michael Lobstein - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventListener {
|
||||
private static final long RECON_POLLING_INTERVAL_SEC = 60;
|
||||
private static final long POLLING_INTERVAL_SEC = 30;
|
||||
private static final long CLOCK_SYNC_INTERVAL_SEC = 3600;
|
||||
private static final long INITIAL_POLLING_DELAY_SEC = 30;
|
||||
private static final long INITIAL_CLOCK_SYNC_DELAY_SEC = 10;
|
||||
// spec says wait 50ms, min is 100
|
||||
private static final long SLEEP_BETWEEN_CMD_MS = 100;
|
||||
private static final Unit<Time> API_SECOND_UNIT = SmartHomeUnits.SECOND;
|
||||
|
||||
private static final String ZONE = "ZONE";
|
||||
private static final String SOURCE = "SOURCE";
|
||||
private static final String CHANNEL_DELIMIT = "#";
|
||||
private static final String UNDEF = "UNDEF";
|
||||
private static final String GC_STR = "NV-IG8";
|
||||
|
||||
private static final int MAX_ZONES = 20;
|
||||
private static final int MAX_SRC = 6;
|
||||
private static final int MIN_VOLUME = 0;
|
||||
private static final int MAX_VOLUME = 79;
|
||||
private static final int MIN_EQ = -18;
|
||||
private static final int MAX_EQ = 18;
|
||||
|
||||
private static final Pattern ZONE_PATTERN = Pattern
|
||||
.compile("^ON,SRC(\\d{1}),(MUTE|VOL\\d{1,2}),DND([0-1]),LOCK([0-1])$");
|
||||
private static final Pattern DISP_PATTERN = Pattern.compile("^DISPLINE(\\d{1}),\"(.*)\"$");
|
||||
private static final Pattern DISP_INFO_PATTERN = Pattern
|
||||
.compile("^DISPINFO,DUR(\\d{1,6}),POS(\\d{1,6}),STATUS(\\d{1,2})$");
|
||||
private static final Pattern ZONE_CFG_PATTERN = Pattern.compile("^BASS(.*),TREB(.*),BAL(.*),LOUDCMP([0-1])$");
|
||||
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy,MM,dd,HH,mm");
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NuvoHandler.class);
|
||||
private final NuvoStateDescriptionOptionProvider stateDescriptionProvider;
|
||||
private final SerialPortManager serialPortManager;
|
||||
|
||||
private @Nullable ScheduledFuture<?> reconnectJob;
|
||||
private @Nullable ScheduledFuture<?> pollingJob;
|
||||
private @Nullable ScheduledFuture<?> clockSyncJob;
|
||||
|
||||
private NuvoConnector connector = new NuvoDefaultConnector();
|
||||
private long lastEventReceived = System.currentTimeMillis();
|
||||
private int numZones = 1;
|
||||
private String versionString = BLANK;
|
||||
private boolean isGConcerto = false;
|
||||
private Object sequenceLock = new Object();
|
||||
|
||||
Set<Integer> activeZones = new HashSet<>(1);
|
||||
|
||||
// A state option list for the source labels
|
||||
List<StateOption> sourceLabels = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public NuvoHandler(Thing thing, NuvoStateDescriptionOptionProvider stateDescriptionProvider,
|
||||
SerialPortManager serialPortManager) {
|
||||
super(thing);
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
this.serialPortManager = serialPortManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
final String uid = this.getThing().getUID().getAsString();
|
||||
NuvoThingConfiguration config = getConfigAs(NuvoThingConfiguration.class);
|
||||
final String serialPort = config.serialPort;
|
||||
final String host = config.host;
|
||||
final Integer port = config.port;
|
||||
final Integer numZones = config.numZones;
|
||||
|
||||
// Check configuration settings
|
||||
String configError = null;
|
||||
if ((serialPort == null || serialPort.isEmpty()) && (host == null || host.isEmpty())) {
|
||||
configError = "undefined serialPort and host configuration settings; please set one of them";
|
||||
} else if (serialPort != null && (host == null || host.isEmpty())) {
|
||||
if (serialPort.toLowerCase().startsWith("rfc2217")) {
|
||||
configError = "use host and port configuration settings for a serial over IP connection";
|
||||
}
|
||||
} else {
|
||||
if (port == null) {
|
||||
configError = "undefined port configuration setting";
|
||||
} else if (port <= 0) {
|
||||
configError = "invalid port configuration setting";
|
||||
}
|
||||
}
|
||||
|
||||
if (configError != null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (serialPort != null) {
|
||||
connector = new NuvoSerialConnector(serialPortManager, serialPort, uid);
|
||||
} else if (port != null) {
|
||||
connector = new NuvoIpConnector(host, port, uid);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Either Serial port or Host & Port must be specifed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (numZones != null) {
|
||||
this.numZones = numZones;
|
||||
}
|
||||
|
||||
activeZones = IntStream.range((1), (this.numZones + 1)).boxed().collect(Collectors.toSet());
|
||||
|
||||
// remove the channels for the zones we are not using
|
||||
if (this.numZones < MAX_ZONES) {
|
||||
List<Channel> channels = new ArrayList<>(this.getThing().getChannels());
|
||||
|
||||
List<Integer> zonesToRemove = IntStream.range((this.numZones + 1), (MAX_ZONES + 1)).boxed()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
zonesToRemove.forEach(zone -> channels.removeIf(c -> (c.getUID().getId().contains("zone" + zone))));
|
||||
updateThing(editThing().withChannels(channels).build());
|
||||
}
|
||||
|
||||
if (config.clockSync) {
|
||||
scheduleClockSyncJob();
|
||||
}
|
||||
|
||||
scheduleReconnectJob();
|
||||
schedulePollingJob();
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
cancelReconnectJob();
|
||||
cancelPollingJob();
|
||||
cancelClockSyncJob();
|
||||
closeConnection();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Collections.singletonList(NuvoThingActions.class);
|
||||
}
|
||||
|
||||
public void handleRawCommand(@Nullable String command) {
|
||||
synchronized (sequenceLock) {
|
||||
try {
|
||||
connector.sendCommand(command);
|
||||
} catch (NuvoException e) {
|
||||
logger.warn("Nuvo Command: {} failed", command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a command the UI
|
||||
*
|
||||
* @param channelUID the channel sending the command
|
||||
* @param command the command received
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
String channel = channelUID.getId();
|
||||
String[] channelSplit = channel.split(CHANNEL_DELIMIT);
|
||||
NuvoEnum target = NuvoEnum.valueOf(channelSplit[0].toUpperCase());
|
||||
|
||||
String channelType = channelSplit[1];
|
||||
|
||||
if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (sequenceLock) {
|
||||
if (!connector.isConnected()) {
|
||||
logger.warn("Command {} from channel {} is ignored: connection not established", command, channel);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (channelType) {
|
||||
case CHANNEL_TYPE_POWER:
|
||||
if (command instanceof OnOffType) {
|
||||
connector.sendCommand(target, command == OnOffType.ON ? NuvoCommand.ON : NuvoCommand.OFF);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TYPE_SOURCE:
|
||||
if (command instanceof DecimalType) {
|
||||
int value = ((DecimalType) command).intValue();
|
||||
if (value >= 1 && value <= MAX_SRC) {
|
||||
logger.debug("Got source command {} zone {}", value, target);
|
||||
connector.sendCommand(target, NuvoCommand.SOURCE, String.valueOf(value));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TYPE_VOLUME:
|
||||
if (command instanceof PercentType) {
|
||||
int value = (MAX_VOLUME
|
||||
- (int) Math.round(
|
||||
((PercentType) command).doubleValue() / 100.0 * (MAX_VOLUME - MIN_VOLUME))
|
||||
+ MIN_VOLUME);
|
||||
logger.debug("Got volume command {} zone {}", value, target);
|
||||
connector.sendCommand(target, NuvoCommand.VOLUME, String.valueOf(value));
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TYPE_MUTE:
|
||||
if (command instanceof OnOffType) {
|
||||
connector.sendCommand(target,
|
||||
command == OnOffType.ON ? NuvoCommand.MUTE_ON : NuvoCommand.MUTE_OFF);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TYPE_TREBLE:
|
||||
if (command instanceof DecimalType) {
|
||||
int value = ((DecimalType) command).intValue();
|
||||
if (value >= MIN_EQ && value <= MAX_EQ) {
|
||||
// device can only accept even values
|
||||
if (value % 2 == 1)
|
||||
value++;
|
||||
logger.debug("Got treble command {} zone {}", value, target);
|
||||
connector.sendCfgCommand(target, NuvoCommand.TREBLE, String.valueOf(value));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TYPE_BASS:
|
||||
if (command instanceof DecimalType) {
|
||||
int value = ((DecimalType) command).intValue();
|
||||
if (value >= MIN_EQ && value <= MAX_EQ) {
|
||||
if (value % 2 == 1)
|
||||
value++;
|
||||
logger.debug("Got bass command {} zone {}", value, target);
|
||||
connector.sendCfgCommand(target, NuvoCommand.BASS, String.valueOf(value));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TYPE_BALANCE:
|
||||
if (command instanceof DecimalType) {
|
||||
int value = ((DecimalType) command).intValue();
|
||||
if (value >= MIN_EQ && value <= MAX_EQ) {
|
||||
if (value % 2 == 1)
|
||||
value++;
|
||||
logger.debug("Got balance command {} zone {}", value, target);
|
||||
connector.sendCfgCommand(target, NuvoCommand.BALANCE,
|
||||
NuvoStatusCodes.getBalanceFromInt(value));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TYPE_LOUDNESS:
|
||||
if (command instanceof OnOffType) {
|
||||
connector.sendCfgCommand(target, NuvoCommand.LOUDNESS,
|
||||
command == OnOffType.ON ? ONE : ZERO);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TYPE_CONTROL:
|
||||
handleControlCommand(target, command);
|
||||
break;
|
||||
case CHANNEL_TYPE_DND:
|
||||
if (command instanceof OnOffType) {
|
||||
connector.sendCommand(target,
|
||||
command == OnOffType.ON ? NuvoCommand.DND_ON : NuvoCommand.DND_OFF);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TYPE_PARTY:
|
||||
if (command instanceof OnOffType) {
|
||||
connector.sendCommand(target,
|
||||
command == OnOffType.ON ? NuvoCommand.PARTY_ON : NuvoCommand.PARTY_OFF);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_DISPLAY_LINE1:
|
||||
if (command instanceof StringType) {
|
||||
connector.sendCommand(target, NuvoCommand.DISPLINE1, "\"" + command + "\"");
|
||||
}
|
||||
break;
|
||||
case CHANNEL_DISPLAY_LINE2:
|
||||
if (command instanceof StringType) {
|
||||
connector.sendCommand(target, NuvoCommand.DISPLINE2, "\"" + command + "\"");
|
||||
}
|
||||
break;
|
||||
case CHANNEL_DISPLAY_LINE3:
|
||||
if (command instanceof StringType) {
|
||||
connector.sendCommand(target, NuvoCommand.DISPLINE3, "\"" + command + "\"");
|
||||
}
|
||||
break;
|
||||
case CHANNEL_DISPLAY_LINE4:
|
||||
if (command instanceof StringType) {
|
||||
connector.sendCommand(target, NuvoCommand.DISPLINE4, "\"" + command + "\"");
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TYPE_ALLOFF:
|
||||
if (command instanceof OnOffType) {
|
||||
connector.sendCommand(NuvoCommand.ALLOFF);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TYPE_ALLMUTE:
|
||||
if (command instanceof OnOffType) {
|
||||
connector.sendCommand(target,
|
||||
command == OnOffType.ON ? NuvoCommand.ALLMUTE_ON : NuvoCommand.ALLMUTE_OFF);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TYPE_PAGE:
|
||||
if (command instanceof OnOffType) {
|
||||
connector.sendCommand(target,
|
||||
command == OnOffType.ON ? NuvoCommand.PAGE_ON : NuvoCommand.PAGE_OFF);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (NuvoException e) {
|
||||
logger.warn("Command {} from channel {} failed: {}", command, channel, e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Sending command failed");
|
||||
closeConnection();
|
||||
scheduleReconnectJob();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the connection with the Nuvo device
|
||||
*
|
||||
* @return true if the connection is opened successfully or false if not
|
||||
*/
|
||||
private synchronized boolean openConnection() {
|
||||
connector.addEventListener(this);
|
||||
try {
|
||||
connector.open();
|
||||
} catch (NuvoException e) {
|
||||
logger.debug("openConnection() failed: {}", e.getMessage());
|
||||
}
|
||||
logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
|
||||
return connector.isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the connection with the Nuvo device
|
||||
*/
|
||||
private synchronized void closeConnection() {
|
||||
if (connector.isConnected()) {
|
||||
connector.close();
|
||||
connector.removeEventListener(this);
|
||||
logger.debug("closeConnection(): disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an event received from the Nuvo device
|
||||
*
|
||||
* @param event the event to process
|
||||
*/
|
||||
@Override
|
||||
public void onNewMessageEvent(NuvoMessageEvent evt) {
|
||||
logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
|
||||
lastEventReceived = System.currentTimeMillis();
|
||||
|
||||
String type = evt.getType();
|
||||
String key = evt.getKey();
|
||||
String updateData = evt.getValue().trim();
|
||||
if (this.getThing().getStatus() == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.versionString);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case TYPE_VERSION:
|
||||
this.versionString = updateData;
|
||||
// Determine if we are a Grand Concerto or not
|
||||
if (this.versionString.contains(GC_STR)) {
|
||||
this.isGConcerto = true;
|
||||
connector.setEssentia(false);
|
||||
}
|
||||
break;
|
||||
case TYPE_ALLOFF:
|
||||
activeZones.forEach(zoneNum -> {
|
||||
updateChannelState(NuvoEnum.valueOf(ZONE + zoneNum), CHANNEL_TYPE_POWER, OFF);
|
||||
});
|
||||
break;
|
||||
case TYPE_ALLMUTE:
|
||||
updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_ALLMUTE, ONE.equals(updateData) ? ON : OFF);
|
||||
activeZones.forEach(zoneNum -> {
|
||||
updateChannelState(NuvoEnum.valueOf(ZONE + zoneNum), CHANNEL_TYPE_MUTE,
|
||||
ONE.equals(updateData) ? ON : OFF);
|
||||
});
|
||||
break;
|
||||
case TYPE_PAGE:
|
||||
updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_PAGE, ONE.equals(updateData) ? ON : OFF);
|
||||
break;
|
||||
case TYPE_SOURCE_UPDATE:
|
||||
logger.debug("Source update: Source: {} - Value: {}", key, updateData);
|
||||
NuvoEnum targetSource = NuvoEnum.valueOf(SOURCE + key);
|
||||
|
||||
if (updateData.contains(DISPLINE)) {
|
||||
// example: DISPLINE2,"Play My Song (Featuring Dee Ajayi)"
|
||||
Matcher matcher = DISP_PATTERN.matcher(updateData);
|
||||
if (matcher.find()) {
|
||||
updateChannelState(targetSource, CHANNEL_DISPLAY_LINE + matcher.group(1), matcher.group(2));
|
||||
} else {
|
||||
logger.debug("no match on message: {}", updateData);
|
||||
}
|
||||
} else if (updateData.contains(DISPINFO)) {
|
||||
// example: DISPINFO,DUR0,POS70,STATUS2 (DUR and POS are expressed in tenths of a second)
|
||||
// 6 places(tenths of a second)-> max 999,999 /10/60/60/24 = 1.15 days
|
||||
Matcher matcher = DISP_INFO_PATTERN.matcher(updateData);
|
||||
if (matcher.find()) {
|
||||
updateChannelState(targetSource, CHANNEL_TRACK_LENGTH, matcher.group(1));
|
||||
updateChannelState(targetSource, CHANNEL_TRACK_POSITION, matcher.group(2));
|
||||
updateChannelState(targetSource, CHANNEL_PLAY_MODE, matcher.group(3));
|
||||
} else {
|
||||
logger.debug("no match on message: {}", updateData);
|
||||
}
|
||||
} else if (updateData.contains(NAME_QUOTE) && sourceLabels.size() <= MAX_SRC) {
|
||||
// example: NAME"Ipod"
|
||||
String name = updateData.split("\"")[1];
|
||||
sourceLabels.add(new StateOption(key, name));
|
||||
}
|
||||
break;
|
||||
case TYPE_ZONE_UPDATE:
|
||||
logger.debug("Zone update: Zone: {} - Value: {}", key, updateData);
|
||||
// example : OFF
|
||||
// or: ON,SRC3,VOL63,DND0,LOCK0
|
||||
// or: ON,SRC3,MUTE,DND0,LOCK0
|
||||
|
||||
NuvoEnum targetZone = NuvoEnum.valueOf(ZONE + key);
|
||||
|
||||
if (OFF.equals(updateData)) {
|
||||
updateChannelState(targetZone, CHANNEL_TYPE_POWER, OFF);
|
||||
updateChannelState(targetZone, CHANNEL_TYPE_SOURCE, UNDEF);
|
||||
} else {
|
||||
Matcher matcher = ZONE_PATTERN.matcher(updateData);
|
||||
if (matcher.find()) {
|
||||
updateChannelState(targetZone, CHANNEL_TYPE_POWER, ON);
|
||||
updateChannelState(targetZone, CHANNEL_TYPE_SOURCE, matcher.group(1));
|
||||
|
||||
if (MUTE.equals(matcher.group(2))) {
|
||||
updateChannelState(targetZone, CHANNEL_TYPE_MUTE, ON);
|
||||
} else {
|
||||
updateChannelState(targetZone, CHANNEL_TYPE_MUTE, NuvoCommand.OFF.getValue());
|
||||
updateChannelState(targetZone, CHANNEL_TYPE_VOLUME, matcher.group(2).replace(VOL, BLANK));
|
||||
}
|
||||
|
||||
updateChannelState(targetZone, CHANNEL_TYPE_DND, ONE.equals(matcher.group(3)) ? ON : OFF);
|
||||
updateChannelState(targetZone, CHANNEL_TYPE_LOCK, ONE.equals(matcher.group(4)) ? ON : OFF);
|
||||
} else {
|
||||
logger.debug("no match on message: {}", updateData);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case TYPE_ZONE_BUTTON:
|
||||
logger.debug("Zone Button pressed: Source: {} - Button: {}", key, updateData);
|
||||
updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, updateData);
|
||||
break;
|
||||
case TYPE_ZONE_CONFIG:
|
||||
logger.debug("Zone Configuration: Zone: {} - Value: {}", key, updateData);
|
||||
// example: BASS1,TREB-2,BALR2,LOUDCMP1
|
||||
Matcher matcher = ZONE_CFG_PATTERN.matcher(updateData);
|
||||
if (matcher.find()) {
|
||||
updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_BASS, matcher.group(1));
|
||||
updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_TREBLE, matcher.group(2));
|
||||
updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_BALANCE,
|
||||
NuvoStatusCodes.getBalanceFromStr(matcher.group(3)));
|
||||
updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_LOUDNESS,
|
||||
ONE.equals(matcher.group(4)) ? ON : OFF);
|
||||
} else {
|
||||
logger.debug("no match on message: {}", updateData);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.debug("onNewMessageEvent: unhandled key {}", key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the reconnection job
|
||||
*/
|
||||
private void scheduleReconnectJob() {
|
||||
logger.debug("Schedule reconnect job");
|
||||
cancelReconnectJob();
|
||||
reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
if (!connector.isConnected()) {
|
||||
logger.debug("Trying to reconnect...");
|
||||
closeConnection();
|
||||
String error = null;
|
||||
if (openConnection()) {
|
||||
synchronized (sequenceLock) {
|
||||
try {
|
||||
long prevUpdateTime = lastEventReceived;
|
||||
|
||||
connector.sendCommand(NuvoCommand.GET_CONTROLLER_VERSION);
|
||||
|
||||
NuvoEnum.VALID_SOURCES.forEach(source -> {
|
||||
try {
|
||||
connector.sendQuery(NuvoEnum.valueOf(source), NuvoCommand.NAME);
|
||||
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
|
||||
connector.sendQuery(NuvoEnum.valueOf(source), NuvoCommand.DISPINFO);
|
||||
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
|
||||
connector.sendQuery(NuvoEnum.valueOf(source), NuvoCommand.DISPLINE);
|
||||
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
|
||||
} catch (NuvoException | InterruptedException e) {
|
||||
logger.debug("Error Querying Source data: {}", e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
// Query all active zones to get their current status and eq configuration
|
||||
activeZones.forEach(zoneNum -> {
|
||||
try {
|
||||
connector.sendQuery(NuvoEnum.valueOf(ZONE + zoneNum), NuvoCommand.STATUS);
|
||||
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
|
||||
connector.sendCfgCommand(NuvoEnum.valueOf(ZONE + zoneNum), NuvoCommand.EQ_QUERY,
|
||||
BLANK);
|
||||
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
|
||||
} catch (NuvoException | InterruptedException e) {
|
||||
logger.debug("Error Querying Zone data: {}", e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
// prevUpdateTime should have changed if a zone update was received
|
||||
if (prevUpdateTime == lastEventReceived) {
|
||||
error = "Controller not responding to status requests";
|
||||
} else {
|
||||
// Put the source labels on all active zones
|
||||
activeZones.forEach(zoneNum -> {
|
||||
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(),
|
||||
ZONE.toLowerCase() + zoneNum + CHANNEL_DELIMIT + CHANNEL_TYPE_SOURCE),
|
||||
sourceLabels);
|
||||
});
|
||||
}
|
||||
} catch (NuvoException e) {
|
||||
error = "First command after connection failed";
|
||||
logger.debug("{}: {}", error, e.getMessage());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error = "Reconnection failed";
|
||||
}
|
||||
if (error != null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
|
||||
closeConnection();
|
||||
} else {
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.versionString);
|
||||
}
|
||||
}
|
||||
}, 1, RECON_POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the reconnection job
|
||||
*/
|
||||
private void cancelReconnectJob() {
|
||||
ScheduledFuture<?> reconnectJob = this.reconnectJob;
|
||||
if (reconnectJob != null) {
|
||||
reconnectJob.cancel(true);
|
||||
this.reconnectJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the polling job
|
||||
*/
|
||||
private void schedulePollingJob() {
|
||||
logger.debug("Schedule polling job");
|
||||
cancelPollingJob();
|
||||
|
||||
// when the Nuvo amp is off, this will keep the connection (esp Serial over IP) alive and detect if the
|
||||
// connection goes down
|
||||
pollingJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
if (connector.isConnected()) {
|
||||
logger.debug("Polling the component for updated status...");
|
||||
|
||||
synchronized (sequenceLock) {
|
||||
try {
|
||||
connector.sendCommand(NuvoCommand.GET_CONTROLLER_VERSION);
|
||||
} catch (NuvoException e) {
|
||||
logger.debug("Polling error: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// if the last event received was more than 1.25 intervals ago,
|
||||
// the component is not responding even though the connection is still good
|
||||
if ((System.currentTimeMillis() - lastEventReceived) > (POLLING_INTERVAL_SEC * 1.25 * 1000)) {
|
||||
logger.debug("Component not responding to status requests");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Component not responding to status requests");
|
||||
closeConnection();
|
||||
scheduleReconnectJob();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, INITIAL_POLLING_DELAY_SEC, POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the polling job
|
||||
*/
|
||||
private void cancelPollingJob() {
|
||||
ScheduledFuture<?> pollingJob = this.pollingJob;
|
||||
if (pollingJob != null) {
|
||||
pollingJob.cancel(true);
|
||||
this.pollingJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the clock sync job
|
||||
*/
|
||||
private void scheduleClockSyncJob() {
|
||||
logger.debug("Schedule clock sync job");
|
||||
cancelClockSyncJob();
|
||||
clockSyncJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
if (this.isGConcerto) {
|
||||
try {
|
||||
connector.sendCommand(NuvoCommand.CFGTIME.getValue() + DATE_FORMAT.format(new Date()));
|
||||
} catch (NuvoException e) {
|
||||
logger.debug("Error syncing clock: {}", e.getMessage());
|
||||
}
|
||||
} else {
|
||||
this.cancelClockSyncJob();
|
||||
}
|
||||
}, INITIAL_CLOCK_SYNC_DELAY_SEC, CLOCK_SYNC_INTERVAL_SEC, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the clock sync job
|
||||
*/
|
||||
private void cancelClockSyncJob() {
|
||||
ScheduledFuture<?> clockSyncJob = this.clockSyncJob;
|
||||
if (clockSyncJob != null) {
|
||||
clockSyncJob.cancel(true);
|
||||
this.clockSyncJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of a channel
|
||||
*
|
||||
* @param target the channel group
|
||||
* @param channelType the channel group item
|
||||
* @param value the value to be updated
|
||||
*/
|
||||
private void updateChannelState(NuvoEnum target, String channelType, String value) {
|
||||
String channel = target.name().toLowerCase() + CHANNEL_DELIMIT + channelType;
|
||||
|
||||
if (!isLinked(channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
State state = UnDefType.UNDEF;
|
||||
|
||||
if (UNDEF.equals(value)) {
|
||||
updateState(channel, state);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channelType) {
|
||||
case CHANNEL_TYPE_POWER:
|
||||
case CHANNEL_TYPE_MUTE:
|
||||
case CHANNEL_TYPE_DND:
|
||||
case CHANNEL_TYPE_PARTY:
|
||||
case CHANNEL_TYPE_ALLMUTE:
|
||||
case CHANNEL_TYPE_PAGE:
|
||||
case CHANNEL_TYPE_LOUDNESS:
|
||||
state = ON.equals(value) ? OnOffType.ON : OnOffType.OFF;
|
||||
break;
|
||||
case CHANNEL_TYPE_LOCK:
|
||||
state = ON.equals(value) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
|
||||
break;
|
||||
case CHANNEL_TYPE_SOURCE:
|
||||
case CHANNEL_TYPE_TREBLE:
|
||||
case CHANNEL_TYPE_BASS:
|
||||
case CHANNEL_TYPE_BALANCE:
|
||||
state = new DecimalType(value);
|
||||
break;
|
||||
case CHANNEL_TYPE_VOLUME:
|
||||
int volume = Integer.parseInt(value);
|
||||
long volumePct = Math
|
||||
.round((double) (MAX_VOLUME - volume) / (double) (MAX_VOLUME - MIN_VOLUME) * 100.0);
|
||||
state = new PercentType(BigDecimal.valueOf(volumePct));
|
||||
break;
|
||||
case CHANNEL_DISPLAY_LINE1:
|
||||
case CHANNEL_DISPLAY_LINE2:
|
||||
case CHANNEL_DISPLAY_LINE3:
|
||||
case CHANNEL_DISPLAY_LINE4:
|
||||
case CHANNEL_BUTTON_PRESS:
|
||||
state = new StringType(value);
|
||||
break;
|
||||
case CHANNEL_PLAY_MODE:
|
||||
state = new StringType(NuvoStatusCodes.PLAY_MODE.get(value));
|
||||
break;
|
||||
case CHANNEL_TRACK_LENGTH:
|
||||
case CHANNEL_TRACK_POSITION:
|
||||
state = new QuantityType<Time>(Integer.parseInt(value) / 10, NuvoHandler.API_SECOND_UNIT);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
updateState(channel, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a button press from a UI Player item
|
||||
*
|
||||
* @param target the nuvo zone to receive the command
|
||||
* @param command the button press command to send to the zone
|
||||
*/
|
||||
private void handleControlCommand(NuvoEnum target, Command command) throws NuvoException {
|
||||
if (command instanceof PlayPauseType) {
|
||||
connector.sendCommand(target, NuvoCommand.PLAYPAUSE);
|
||||
} else if (command instanceof NextPreviousType) {
|
||||
if (command == NextPreviousType.NEXT) {
|
||||
connector.sendCommand(target, NuvoCommand.NEXT);
|
||||
} else if (command == NextPreviousType.PREVIOUS) {
|
||||
connector.sendCommand(target, NuvoCommand.PREV);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unknown control command: {}", command);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="nuvo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>Nuvo Whole House Audio Binding</name>
|
||||
<description>Controls the Nuvo Grand Concerto or Essentia G Whole House Amplifier.</description>
|
||||
<author>Michael Lobstein</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,328 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="nuvo"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<!-- Nuvo Whole House Amplifier Thing -->
|
||||
<thing-type id="amplifier">
|
||||
<label>Whole House Amplifier</label>
|
||||
<description>
|
||||
A Multi-zone Whole House Amplifier System
|
||||
</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="system" typeId="system">
|
||||
<label>System</label>
|
||||
<description>System Level Commands</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone1" typeId="zone">
|
||||
<label>Zone 1</label>
|
||||
<description>The Controls for Zone 1</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone2" typeId="zone">
|
||||
<label>Zone 2</label>
|
||||
<description>The Controls for Zone 2</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone3" typeId="zone">
|
||||
<label>Zone 3</label>
|
||||
<description>The Controls for Zone 3</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone4" typeId="zone">
|
||||
<label>Zone 4</label>
|
||||
<description>The Controls for Zone 4</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone5" typeId="zone">
|
||||
<label>Zone 5</label>
|
||||
<description>The Controls for Zone 5</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone6" typeId="zone">
|
||||
<label>Zone 6</label>
|
||||
<description>The Controls for Zone 6</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone7" typeId="zone">
|
||||
<label>Zone 7</label>
|
||||
<description>The Controls for Zone 7</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone8" typeId="zone">
|
||||
<label>Zone 8</label>
|
||||
<description>The Controls for Zone 8</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone9" typeId="zone">
|
||||
<label>Zone 9</label>
|
||||
<description>The Controls for Zone 9</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone10" typeId="zone">
|
||||
<label>Zone 10</label>
|
||||
<description>The Controls for Zone 10</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone11" typeId="zone">
|
||||
<label>Zone 11</label>
|
||||
<description>The Controls for Zone 11</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone12" typeId="zone">
|
||||
<label>Zone 12</label>
|
||||
<description>The Controls for Zone 12</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone13" typeId="zone">
|
||||
<label>Zone 13</label>
|
||||
<description>The Controls for Zone 13</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone14" typeId="zone">
|
||||
<label>Zone 14</label>
|
||||
<description>The Controls for Zone 14</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone15" typeId="zone">
|
||||
<label>Zone 15</label>
|
||||
<description>The Controls for Zone 15</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone16" typeId="zone">
|
||||
<label>Zone 16</label>
|
||||
<description>The Controls for Zone 16</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone17" typeId="zone">
|
||||
<label>Zone 17</label>
|
||||
<description>The Controls for Zone 17</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone18" typeId="zone">
|
||||
<label>Zone 18</label>
|
||||
<description>The Controls for Zone 18</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone19" typeId="zone">
|
||||
<label>Zone 19</label>
|
||||
<description>The Controls for Zone 19</description>
|
||||
</channel-group>
|
||||
<channel-group id="zone20" typeId="zone">
|
||||
<label>Zone 20</label>
|
||||
<description>The Controls for Zone 20</description>
|
||||
</channel-group>
|
||||
<channel-group id="source1" typeId="source">
|
||||
<label>Source 1</label>
|
||||
<description>The Display Information for Source 1</description>
|
||||
</channel-group>
|
||||
<channel-group id="source2" typeId="source">
|
||||
<label>Source 2</label>
|
||||
<description>The Display Information for Source 2</description>
|
||||
</channel-group>
|
||||
<channel-group id="source3" typeId="source">
|
||||
<label>Source 3</label>
|
||||
<description>The Display Information for Source 3</description>
|
||||
</channel-group>
|
||||
<channel-group id="source4" typeId="source">
|
||||
<label>Source 4</label>
|
||||
<description>The Display Information for Source 4</description>
|
||||
</channel-group>
|
||||
<channel-group id="source5" typeId="source">
|
||||
<label>Source 5</label>
|
||||
<description>The Display Information for Source 5</description>
|
||||
</channel-group>
|
||||
<channel-group id="source6" typeId="source">
|
||||
<label>Source 6</label>
|
||||
<description>The Display Information for Source 6</description>
|
||||
</channel-group>
|
||||
</channel-groups>
|
||||
|
||||
<config-description>
|
||||
<parameter name="serialPort" type="text" required="false">
|
||||
<context>serial-port</context>
|
||||
<label>Serial Port</label>
|
||||
<description>Serial Port to use for connecting to the Nuvo amplifier</description>
|
||||
</parameter>
|
||||
<parameter name="host" type="text" required="false">
|
||||
<context>network-address</context>
|
||||
<label>Address</label>
|
||||
<description>Host Name or IP Address of the machine connected to the Nuvo amplifier (Serial over IP)</description>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer" min="1" max="65535" required="false">
|
||||
<label>Port</label>
|
||||
<description>Communication Port (serial over IP). For IP connection to the Nuvo amplifier</description>
|
||||
<default>4444</default>
|
||||
</parameter>
|
||||
<parameter name="numZones" type="integer" min="1" max="20" required="true">
|
||||
<label>Number of Zones</label>
|
||||
<description>Number of Zones on the amplifier to utilize in the binding (Up to 20 zones when using expansion module)</description>
|
||||
<default>6</default>
|
||||
</parameter>
|
||||
<parameter name="clockSync" type="boolean" required="false">
|
||||
<label>Sync Clock On GConcerto</label>
|
||||
<description>If set to true, the binding will sync the internal clock on the Grand Concerto to match the openHAB
|
||||
host's system clock. The sync job runs at binding startup and once an hour thereafter. The Essentia G has no RTC,
|
||||
so this setting has no effect on that component.</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-group-type id="system">
|
||||
<label>System</label>
|
||||
<description>System Level Commands</description>
|
||||
<channels>
|
||||
<channel id="alloff" typeId="alloff"/>
|
||||
<channel id="allmute" typeId="system.mute"/>
|
||||
<channel id="page" typeId="page"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="zone">
|
||||
<label>Zone Controls</label>
|
||||
<description>The Controls for the Zone</description>
|
||||
<channels>
|
||||
<channel id="power" typeId="system.power"/>
|
||||
<channel id="source" typeId="source"/>
|
||||
<channel id="volume" typeId="system.volume"/>
|
||||
<channel id="mute" typeId="system.mute"/>
|
||||
<channel id="control" typeId="control"/>
|
||||
<channel id="treble" typeId="treble"/>
|
||||
<channel id="bass" typeId="bass"/>
|
||||
<channel id="balance" typeId="balance"/>
|
||||
<channel id="loudness" typeId="loudness"/>
|
||||
<channel id="dnd" typeId="dnd"/>
|
||||
<channel id="lock" typeId="lock"/>
|
||||
<channel id="party" typeId="party"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="source">
|
||||
<label>Source Info</label>
|
||||
<description>The Display Information for the Source</description>
|
||||
<channels>
|
||||
<channel id="display_line1" typeId="display_line1"/>
|
||||
<channel id="display_line2" typeId="display_line2"/>
|
||||
<channel id="display_line3" typeId="display_line3"/>
|
||||
<channel id="display_line4" typeId="display_line4"/>
|
||||
<channel id="play_mode" typeId="play_mode"/>
|
||||
<channel id="track_length" typeId="track_length"/>
|
||||
<channel id="track_position" typeId="track_position"/>
|
||||
<channel id="button_press" typeId="button_press"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="alloff">
|
||||
<item-type>Switch</item-type>
|
||||
<label>All Off</label>
|
||||
<description>Turn All Zones Off</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="page">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Page</label>
|
||||
<description>Activates the Page Mode for All Zones</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="source">
|
||||
<item-type>Number</item-type>
|
||||
<label>Source Input</label>
|
||||
<description>Select the Source Input for the Zone</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="control">
|
||||
<item-type>Player</item-type>
|
||||
<label>Control</label>
|
||||
<description>Transport Controls e.g. Play/Pause/Next/Previous for the Current Source</description>
|
||||
<category>Player</category>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="treble">
|
||||
<item-type>Number</item-type>
|
||||
<label>Treble Adjustment</label>
|
||||
<description>Adjust the Treble Setting for the Zone</description>
|
||||
<state min="-18" max="18" step="2" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="bass">
|
||||
<item-type>Number</item-type>
|
||||
<label>Bass Adjustment</label>
|
||||
<description>Adjust the Bass Setting for the Zone</description>
|
||||
<state min="-18" max="18" step="2" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="balance">
|
||||
<item-type>Number</item-type>
|
||||
<label>Balance Adjustment</label>
|
||||
<description>Adjust the Balance Setting for the Zone</description>
|
||||
<state min="-18" max="18" step="2" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="loudness">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Loudness Compensation</label>
|
||||
<description>A Switch That Controls the Loudness Compensation Setting for the Zone</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="dnd">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Do Not Disturb</label>
|
||||
<description>A Switch That Controls If the Zone Should Ignore an Incoming Audio Page</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="lock">
|
||||
<item-type>Contact</item-type>
|
||||
<label>Locked</label>
|
||||
<description>Indicates If This Zone Is Locked</description>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="CLOSED">Unlocked</option>
|
||||
<option value="OPEN">Locked</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="party">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Party Mode</label>
|
||||
<description>Activate Party Mode With This Zone as the Host</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="display_line1">
|
||||
<item-type>String</item-type>
|
||||
<label>Display Line 1</label>
|
||||
<description>1st Line of Text Being Displayed on the Keypad</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="display_line2">
|
||||
<item-type>String</item-type>
|
||||
<label>Display Line 2</label>
|
||||
<description>2nd Line of Text Being Displayed on the Keypad</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="display_line3">
|
||||
<item-type>String</item-type>
|
||||
<label>Display Line 3</label>
|
||||
<description>3rd Line of Text Being Displayed on the Keypad</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="display_line4">
|
||||
<item-type>String</item-type>
|
||||
<label>Display Line 4</label>
|
||||
<description>4th Line of Text Being Displayed on the Keypad</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="play_mode">
|
||||
<item-type>String</item-type>
|
||||
<label>Play Mode</label>
|
||||
<description>The Current Playback Mode of the Source</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="track_length">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Track Length</label>
|
||||
<description>The Total Running Time of the Current Playing Track</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="track_position">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Track Position</label>
|
||||
<description>The Running Time Elapsed of the Current Playing Track</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="button_press">
|
||||
<item-type>String</item-type>
|
||||
<label>Button Pressed</label>
|
||||
<description>Indicates the Last Button Pressed On the Keypad for a Non NuvoNet Source</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user