[nuvo] Add NuvoNet source communication capabilities (#12042)

* Add NuvoNet source communication

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>

* fix readme

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>

* remove commented code

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>

* Add startup/shutdown keypad messages

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>

* Minor cleanup before code review

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>

* Add configurable favorites labels

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>

* add new config item to i18n properties

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>

* Fix restart detection and improve version matching

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>

* review changes

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>

* Increment version number

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>

* remove repeated word in channels.xml

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>

* Review changes

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
mlobstein 2022-09-19 01:00:46 -05:00 committed by GitHub
parent cbe41951fd
commit 89c73a0d81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1402 additions and 109 deletions

View File

@ -1,7 +1,7 @@
# 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).
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 three different kinds of connections:
@ -12,6 +12,7 @@ The binding supports three different kinds of connections:
For users without a serial connector on the server side, you can use a USB to serial adapter.
If you are using the Nuvo MPS4 music server with your Grand Concerto or Essentia G, the binding can connect to the server's IP address on port 5006.
Using the MPS4 connection will also allow for greater interaction with the keypads to include custom menus, custom favorite lists and album art display on the CTP-36 keypad.
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 the LAN (serial over IP).
@ -35,61 +36,83 @@ All settings are through thing configuration parameters.
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 serial port (serial over IP) or MPS4 server | 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 |
| 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 serial port (serial over IP) or MPS4 server | 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) |
| Favorite Labels | favoriteLabels | A comma separated list of up to 12 label names that are loaded into the 'favorites' channel of each zone. These represent keypad favorites 1-12 | Optional; Comma separated list, max 12 items. ie: Favorite 1,Favorite 2,Favorite 3 |
| 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 |
| Source N is NuvoNet | nuvoNetSrcN | MPS4 Only! Indicate if the source is a NuvoNet source in the MPS4 or in openHAB. Nuvo tuners & iPod docks and all others set to 0 | 0 = Non-NuvoNet source, 1 = Source is a used by MPS4, 2 = openHAB NuvoNet Source |
| Source N Favorites | favoritesSrcN | MPS4 Only! A comma separated list of favorite names to load into the global favorites list for Source N. See *very advanced* rules | Comma separated list, max 20 items. Each item max 40 chars, ie: Oldies,Pop,Rock |
| Source N Favorite Prefix | favPrefixN | MPS4 Only! To quickly locate a Source's favorites, this prefix will be added to the favorite names. See *very advanced* rules | Text; ie: 'S2-' will cause the favorite names to be prefixed, e.g. 'S2-Rock' |
| Source N Menu XML | menuXmlSrcN | MPS4 Only! Will load a custom menu for a given source into the keypads. Up to 10 items in the top menu and up to 20 items in each sub menu | XML Text string; see examples below and *very advanced* rules for usage |
Some notes:
* If the port is set to 5006, the binding will adjust its protocol to connect to the Nuvo amplifier thing via an MPS4 IP connection.
* MPS4 connections do not support custom commands using `SxDISPINFO` including those outlined in the advanced rules section below.
* MPS4 connections do not support commands using `SxDISPINFO`& `SxDISPLINE` (display_lineN channels) including those outlined in the advanced rules section below.
* As of OH 3.4.0, the binding supports NuvoNet source communication for any/all of the amplifier's 6 inputs but only when using an MPS4 connection.
* By implementing NuvoNet communication, the binding can now support sending custom menus, custom favorite lists, album art, etc. to the Nuvo keypads for each source configured as an openHAB NuvoNet source.
* 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.
* Source display_line1 thru 4 can only be updated on non NuvoNet sources when not using an MPS4 connection.
* 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):
* Here is an example of ser2net.conf (for ser2net version < 4) 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
```
* Here is an example of ser2net.yaml (for ser2net version >= 4) 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):
```
connection: &conNuvo
accepter: tcp,4444
enable: on
options:
kickolduser: true
connector: serialdev,
/dev/ttyUSB0,
57600n81,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#favorite (where N= 1-20) | Number | Select a preset Favorite for a zone (1-12) |
| 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) |
| 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) |
| system#sendcmd | String | Send a command to the amplifier |
| 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#favorite (where N= 1-20) | Number | Select a preset Favorite for a zone (1-12) |
| 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 amplifier'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 or openHAB NuvoNet source (ReadOnly) |
| sourceN#art_url (where N= 1-6) | String | MPS4 Only! The URL of the Album Art JPG for this source that is displayed on a CTP-36. See *very advanced* rules (SendOnly) |
## Full Example
@ -114,6 +137,7 @@ nuvo.items:
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" }
String nuvo_system_sendcmd "Send Command" { channel="nuvo:amplifier:myamp:system#sendcmd" }
// zones
Switch nuvo_z1_power "Power" { channel="nuvo:amplifier:myamp:zone1#power" }
@ -141,6 +165,7 @@ String nuvo_s1_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:sourc
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_s1_art_url "URL: [%s]" { channel="nuvo:amplifier:myamp:source1#art_url" }
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" }
@ -150,6 +175,7 @@ String nuvo_s2_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:sourc
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_s2_art_url "URL: [%s]" { channel="nuvo:amplifier:myamp:source2#art_url" }
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" }
@ -159,6 +185,7 @@ String nuvo_s3_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:sourc
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_s3_art_url "URL: [%s]" { channel="nuvo:amplifier:myamp:source3#art_url" }
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" }
@ -168,6 +195,7 @@ String nuvo_s4_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:sourc
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_s4_art_url "URL: [%s]" { channel="nuvo:amplifier:myamp:source4#art_url" }
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" }
@ -177,6 +205,7 @@ String nuvo_s5_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:sourc
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_s5_art_url "URL: [%s]" { channel="nuvo:amplifier:myamp:source5#art_url" }
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" }
@ -186,6 +215,7 @@ String nuvo_s6_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:sourc
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" }
// String nuvo_s6_art_url "URL: [%s]" { channel="nuvo:amplifier:myamp:source6#art_url" }
```
@ -205,8 +235,7 @@ sitemap nuvo label="Audio Control" {
// 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"
// mappings is optional to override the default dropdown item labels
Selection item=nuvo_z1_favorite visibility=[nuvo_z1_power==ON] icon="player" //mappings=[1="WNYC", 2="BBC One", 3="My Playlist"]
Selection item=nuvo_z1_favorite visibility=[nuvo_z1_power==ON] icon="player"
Default item=nuvo_z1_control visibility=[nuvo_z1_power==ON]
Text item=nuvo_s1_display_line1 visibility=[nuvo_z1_source=="1"] icon="zoom"
@ -284,6 +313,8 @@ nuvo.rules:
```
import java.text.Normalizer
// To be used with a direct serial port or serial over IP connection
val actions = getActions("nuvo","nuvo:amplifier:myamp")
// send command a custom command to the Nuvo Amplifier
@ -295,7 +326,7 @@ rule "Nuvo Custom Command example"
when
Item SomeItemTrigger received command
then
if(null === actions) {
if (null === actions) {
logInfo("actions", "Actions not found, check thing ID")
return
}
@ -318,7 +349,7 @@ rule "Load track play info for Source 3"
when
Item Item_Containing_TrackLength received update
then
if(null === actions) {
if (null === actions) {
logInfo("actions", "Actions not found, check thing ID")
return
}
@ -335,13 +366,13 @@ 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.)
// The Nuvo keypad cannot display extended ASCII characters (accent, umlaut, 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("")
sendCommand(nuvo_s3_display_line4, trackName)
sendCommand(nuvo_s3_display_line1, "")
end
@ -352,7 +383,7 @@ 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)
sendCommand(nuvo_s3_display_line2, albumName)
end
rule "Load artist name for Source 3"
@ -362,7 +393,7 @@ 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)
sendCommand(nuvo_s3_display_line3, artistName)
end
// In this rule we have three items: Item_Containing_PlayMode, Item_Containing_TrackLength & Item_Containing_TrackPosition
@ -380,7 +411,7 @@ then
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) {
if (null === actions) {
logInfo("actions", "Actions not found, check thing ID")
return
}
@ -404,3 +435,221 @@ then
end
```
### XML Menu Examples
By using an MPS4 connection to the Nuvo amplifier, it is possible to send custom menus for each source that will be displayed in the physical keypads.
When the menu item is selected on the keypad, the text of that menu item will be sent to the button_press channel for the given source.
By using rules, it is possible to execute an action on any other openHAB item as a result of selecting a menu item on the physical keypad.
Below is an example of the XML format that is used to create the menu structure.
Up to 10 top menu items can be added. The string inside the text attribute of the topmenu tag will be displayed.
Each `<topmenu>` item can have up to 20 `<item>` tags contained within.
The topmenu item does not need to have any sub menu items if not desired as seen in the 'Top menu 2' example.
A complete XML string for the desired menu is then stored in the `menuXmlSrcN` configuration parameter for a given source and will be loaded into the Nuvo keypads during binding initialization.
```
<topmenu text="Top menu 1">
<item>menu1 a</item>
<item>menu1 b</item>
<item>menu1 c</item>
</topmenu>
<topmenu text="Top menu 2"/>
<topmenu text="Top menu 3">
<item>menu3 x</item>
<item>menu3 y</item>
</topmenu>
```
When a menu item is selected, the text of the topmenu item and sub menu item (if applicable) will be sent to the button channel in a pipe delimited format.
For example, when item `menu1 b` is selected, the text `Top menu 1|menu1 b` will be sent to the button channel.
When the item `Top menu 2` is selected the text sent to the button channel will simply be `Top menu 2` since this menu item does not have any sub menu items.
### MPS4 openHAB NuvoNet source custom integration rules *(very advanced)*
The following are a set of example rules necessary to integrate metadata and control of another openHAB connected source (ie: Chromecast) into an openHAB NuvoNet source.
By using these rules, it is possible to have artist, album and track names displayed on the keypad, transport button presses from the keypad relayed to the source, and album art displayed if using a Nuvo CTP-36 keypad.
Global Favorites selection and Menu selections from the custom menus described above are also processed by these rules.
The list of favorite names should be playable via another thing connected to openHAB and this thing should have a means to accept a text string that tells it to play a particular favorite/playlist.
nuvo-advanced.rules:
```
import java.text.Normalizer
// all examples using Source 6
var source = "S6"
var artistName = ""
var albumName = ""
var trackName = ""
// supportedactions bitmask tells the keypad what buttons to display
// detailed in SourceCommunicationProtocolForNNA_v1.0.pdf
// 0 : play/pause only
// 196615 : play/pause/skip
// 196639 : play/pause/skip/shuffle/repeat
// 245791 : play/pause/skip/shuffle/repeat/thumbsup/thumbsdown
var supportedActionsMask = "196639"
// a very basic example to display text on all 4 lines and load an example image as album art
rule "Basic keypad communication example rule"
when
System started
then
sendCommand(nuvo_system_sendcmd, source + "DISPLINES0,0,0,\"Hello World\",\"Welcome to openHAB!\",\"Example Text\",\"Displayed On Keypad\"")
sendCommand(nuvo_system_sendcmd, source + "DISPINFOTWO0,0,1,albumartid,2,1,0")
sendCommand(nuvo_s6_art_url, "https://icon-library.com/images/sample-icon/sample-icon-22.jpg")
end
rule "Music Source nuvo button press"
when
Item nuvo_s6_button_press received update
then
var button = nuvo_s6_button_press.state.toString()
// If a favorite is selected it will be prepended for easier identification from other buttons
// ie: 'PLAY_MUSIC_PRESET:Rock'
if (button.startsWith("PLAY_MUSIC_PRESET:")) {
sendCommand(music_Music_PlayFavorite, button.replace("PLAY_MUSIC_PRESET:", ""))
} else {
// these proxy the Nuvo button presses to the appropriate Music Source button press
switch button {
case "PLAYPAUSE": {
sendCommand(music_Music_Control, PAUSE)
}
case "NEXT": {
sendCommand(music_Music_Control, NEXT)
}
case "PREV": {
sendCommand(music_Music_Control, PREVIOUS)
}
case "SHUFFLETOGGLE": {
if (music_Music_Random.state == ON) {
sendCommand(music_Music_Random, OFF)
} else {
sendCommand(music_Music_Random, ON)
}
}
case "REPEATTOGGLE": {
if (music_Music_Repeat.state == ON) {
sendCommand(music_Music_Repeat, OFF)
} else {
sendCommand(music_Music_Repeat, ON)
}
}
// Handle menu item selections
case "Top menu 1|menu1 a": {
logInfo("nuvo src 6", "'Top menu 1, menu 1 a' was selected")
}
case "Top menu 1|menu1 b": {
logInfo("nuvo src 6", "'Top menu 1, menu 1 b' was selected")
}
case "Top menu 2": {
logInfo("nuvo src 6", "'Top menu 2' was selected")
}
}
}
end
rule "Music Source load album art URL to Nuvo Source 6"
when
Item music_Detail_CoverUrl changed // an item that gets updated with the cover art url
then
// when the CoverUrl changes, pass the new JPG image url to the Nuvo binding
// the binding automatically downloads the JPG and converts it to a format that can be displayed on the CTP-36
// smaller images will yield better performance when the binding resizes the image to 80 x 80 pixels
// note that the CTP-36 keypad may crash/reboot if it receives an invalid image
sendCommand(nuvo_s6_art_url, music_Detail_CoverUrl.state.toString)
end
// if album, artist and track names are maintained in different items, these three rules are necessary
// if the names can be received in one item, then this can condense to one rule sending the lines in one DISPLINES message
// the names can be up to 80 characters and should have any embedded double quotes removed
rule "Music Source update album name"
when
Item music_Music_Album received update
then
if (music_Music_Album.state.toString() != "") {
albumName = Normalizer::normalize(music_Music_Album.state.toString, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "")
sendCommand(nuvo_system_sendcmd, source + "DISPLINES0,0,0,\"\",\"" + albumName + "\",\"" + artistName + "\",\"" + trackName + "\"")
}
end
rule "Music Source update artist name"
when
Item music_Music_Artist received update
then
if (music_Music_Artist.state.toString() != "") {
artistName = Normalizer::normalize(music_Music_Artist.state.toString, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "")
sendCommand(nuvo_system_sendcmd, source + "DISPLINES0,0,0,\"\",\"" + albumName + "\",\"" + artistName + "\",\"" + trackName + "\"")
}
end
rule "Music Source update track name"
when
Item music_Music_Track received update
then
if (music_Music_Track.state.toString() != "") {
trackName = Normalizer::normalize(music_Music_Track.state.toString, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "")
sendCommand(nuvo_system_sendcmd, source + "DISPLINES0,0,0,\"\",\"" + albumName + "\",\"" + artistName + "\",\"" + trackName + "\"")
}
end
rule "Music Source update song elapsed time"
when
Item music_Music_TrackPosition received update or
Item music_Music_Random received update or
Item music_Music_Repeat received update
then
var int trackLength = Integer::parseInt(music_Music_TrackLength.state.toString.replaceAll("[\\D]", "")) * 10
// track position should not update continuously to prevent excessive amounts of DISPINFOTWO messages from being sent
// the keypad counts up the time on its own after a DISPINFOTWO message is received
var int trackPosition = Integer::parseInt(music_Music_TrackPosition.state.toString.replaceAll("[\\D]", "")) * 10
var playState = music_Music_PlayMode.state.toString()
var randomMode = music_Music_Random.state
var repeatMode = music_Music_Repeat.state
// the source status mask tells the keypad the button states to display
// sourcestatus masks for play and pause when random and repeat are both off
var playMask = "2"
var pauseMask = "4"
if (randomMode == ON && repeatMode == OFF) {
playMask = "34"
pauseMask = "36"
} else if (randomMode == OFF && repeatMode == ON) {
playMask = "66"
pauseMask = "68"
} else if (randomMode == ON && repeatMode == ON) {
playMask = "98"
pauseMask = "100"
}
// DISPINFOTWO sends track time, play state, album art id, source status, etc. all in one command message
//*SsDISPINFOTWOduration,position,deprecatedstatus,albumartid,sourcemode,sourcestatus,supportedactions
// The binding will automatically substitute the 'albumartid' token with the id of the JPG processed by the `art_url` channel
if (playState == "Playing") {
// first '2' indicates deprecatedstatus = playing, second '2' is sourcemode = Music Server Mode
// The Nuvo keypad will now begin counting up the elapsed time displayed (starting from trackPosition)
// The elapsed time may reset on randomMode & repeatMode toggles unless a current trackPosition is also sent
sendCommand(nuvo_system_sendcmd, source + "DISPINFOTWO" + trackLength.toString() + "," + trackPosition.toString() + ",2,albumartid,2," + playMask + "," + supportedActionsMask)
}
if (playState == "Paused") {
sendCommand(nuvo_system_sendcmd, source + "DISPINFOTWO" + trackLength.toString() + "," + trackPosition.toString() + ",3,albumartid,2," + pauseMask + "," + supportedActionsMask)
}
if (playState == "Stopped") {
// send '0x0' instead of 'albumartid' since no art should be displayed while stopped
sendCommand(nuvo_system_sendcmd, source + "DISPINFOTWO0,0,1,0x0,2,1,0")
}
end
rule "Music Source update song playing info - stopped"
when
Item music_Music_PlayMode changed to "Stopped"
then
sendCommand(nuvo_system_sendcmd, source + "DISPLINES0,0,0,\"\",\"Nothing Playing\",\"\",\"\"")
end
```

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Created with Liquid Technologies Online Tools 1.0 (https://www.liquid-technologies.com) -->
<!-- To regenerate the NuvoMenu DTO from the XSD, use the following command: -->
<!-- xjc -d ../src/main/java -p org.openhab.binding.nuvo.internal.dto NuvoMenu.xsd -->
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb" jaxb:version="2.1">
<xs:element name="menu">
<xs:annotation>
<xs:appinfo>
<jaxb:class name="NuvoMenu"/>
</xs:appinfo>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" name="source">
<xs:complexType>
<xs:sequence minOccurs="0">
<xs:element maxOccurs="unbounded" name="topmenu">
<xs:complexType>
<xs:sequence minOccurs="0">
<xs:element maxOccurs="unbounded" name="item" type="xs:string" />
</xs:sequence>
<xs:attribute name="text" type="xs:string" use="required" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@ -61,6 +61,7 @@ public class NuvoBindingConstants {
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";
public static final String CHANNEL_ART_URL = "art_url";
// Message types
public static final String TYPE_VERSION = "version";
@ -70,20 +71,41 @@ public class NuvoBindingConstants {
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_BUTTON2 = "zone_button2";
public static final String TYPE_ZONE_MENUREQ = "zone_menureq";
public static final String TYPE_MENU_ITEM_SELECTED = "top_menu_button";
public static final String TYPE_ZONE_CONFIG = "zone_config";
public static final String TYPE_ALBUM_ART_REQ = "album_art_req";
public static final String TYPE_ALBUM_ART_FRAG_REQ = "album_art_frag_req";
public static final String TYPE_FAVORITE_REQ = "favorite_req";
// misc
public static final String ON = "ON";
public static final String OFF = "OFF";
public static final String TWO = "2";
public static final String ONE = "1";
public static final String ZERO = "0";
public static final String BLANK = "";
public static final String SPACE = " ";
public static final String COMMA = ",";
public static final String DISPLINE = "DISPLINE";
public static final String DISPINFO = "DISPINFO,"; // yes comma here
public static final String DISP_INFO_TWO = "DISPINFOTWO";
public static final String NAME_QUOTE = "NAME\"";
public static final String MUTE = "MUTE";
public static final String VOL = "VOL";
public static final String OFFSET_ZERO = "0x";
public static final String ZERO_COMMA = "0,0";
// MPS4
public static final String TYPE_PING = "PING";
public static final String TYPE_RESTART = "RESTART";
public static final String DISABLE = "disable";
public static final String ALBUM_ART_ID = "albumartid";
public static final String SRC_KEY = "S";
public static final String ZONE_KEY = "Z";
public static final String ALBUM_ART_AVAILABLE = "ALBUMARTAVAILABLE";
public static final String ALBUM_ART_FRAG = "ALBUMARTFRAG";
public static final String HTTP = "http://";
public static final String HTTPS = "https://";
}

View File

@ -19,7 +19,9 @@ import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.nuvo.internal.handler.NuvoHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
@ -46,6 +48,8 @@ public class NuvoHandlerFactory extends BaseThingHandlerFactory {
private final NuvoStateDescriptionOptionProvider stateDescriptionProvider;
private final HttpClient httpClient;
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
@ -53,9 +57,11 @@ public class NuvoHandlerFactory extends BaseThingHandlerFactory {
@Activate
public NuvoHandlerFactory(final @Reference NuvoStateDescriptionOptionProvider provider,
final @Reference SerialPortManager serialPortManager) {
final @Reference SerialPortManager serialPortManager,
final @Reference HttpClientFactory httpClientFactory) {
this.stateDescriptionProvider = provider;
this.serialPortManager = serialPortManager;
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
@ -63,7 +69,7 @@ public class NuvoHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
return new NuvoHandler(thing, stateDescriptionProvider, serialPortManager);
return new NuvoHandler(thing, stateDescriptionProvider, serialPortManager, httpClient);
}
return null;

View File

@ -41,19 +41,35 @@ public abstract class NuvoConnector {
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 VER_STR_E6 = "#VER\"NV-E6G";
private static final String VER_STR_GC = "#VER\"NV-I8G";
private static final String ALL_OFF = "#ALLOFF";
private static final String MUTE = "#MUTE";
private static final String PAGE = "#PAGE";
private static final String RESTART = "#RESTART\"NuVoNet\"";
private static final String PING = "#PING";
private static final String PING_RESPONSE = "PING";
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_MENUREQ_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})MENUREQ(.*)$");
private static final Pattern ZONE_BUTTON2_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})BUTTON(.*)$");
private static final Pattern ZONE_BUTTONTWO_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})BUTTONTWO(.*)$");
private static final Pattern ZONE_CFG_PATTERN = Pattern.compile("^#ZCFG(\\d{1,2}),(.*)$");
// S2ALBUMARTREQ0x620FD879,80,80,2,0x00C0C0C0,0,0,0,0,1
private static final Pattern ALBUM_ART_REQ = Pattern.compile("^#S(\\d{1})ALBUMARTREQ(.*)$");
// S2ALBUMARTFRAGREQ0x620FD879,0,750
private static final Pattern ALBUM_ART_FRAG_REQ = Pattern.compile("^#S(\\d{1})ALBUMARTFRAGREQ(.*)$");
// S6FAVORITE0x000003E8
private static final Pattern FAVORITE_PATTERN = Pattern.compile("^#S(\\d{1})FAVORITE0x(.*)$");
private final Logger logger = LoggerFactory.getLogger(NuvoConnector.class);
protected static final String COMMAND_ERROR = "#?";
@ -306,11 +322,21 @@ public abstract class NuvoConnector {
}
if (message.contains(PING)) {
try {
sendCommand(PING_RESPONSE);
} catch (NuvoException e) {
logger.debug("Error sending response to PING command");
}
dispatchKeyValue(TYPE_PING, BLANK, BLANK);
return;
}
if (message.contains(VER_STR)) {
if (RESTART.equals(message)) {
dispatchKeyValue(TYPE_RESTART, BLANK, BLANK);
return;
}
if (message.contains(VER_STR_E6) || message.contains(VER_STR_GC)) {
// example: #VER"NV-E6G FWv2.66 HWv0"
// split on " and return the version number
dispatchKeyValue(TYPE_VERSION, "", message.split("\"")[1]);
@ -332,16 +358,40 @@ public abstract class NuvoConnector {
return;
}
// Amp controller send a source update ie: #S2DISPINFO,DUR3380,POS3090,STATUS2
// Amp controller sent an album art request
Matcher matcher = ALBUM_ART_REQ.matcher(message);
if (matcher.find()) {
// pull out the source id and the remainder of the message
dispatchKeyValue(TYPE_ALBUM_ART_REQ, matcher.group(1), matcher.group(2));
return;
}
// Amp controller sent an album art fragment request
matcher = ALBUM_ART_FRAG_REQ.matcher(message);
if (matcher.find()) {
// pull out the source id and the remainder of the message
dispatchKeyValue(TYPE_ALBUM_ART_FRAG_REQ, matcher.group(1), matcher.group(2));
return;
}
// Amp controller sent a request to play a favorite
matcher = FAVORITE_PATTERN.matcher(message);
if (matcher.find()) {
// pull out the source id and the remainder of the message
dispatchKeyValue(TYPE_FAVORITE_REQ, matcher.group(1), matcher.group(2));
return;
}
// Amp controller sent a source update ie: #S2DISPINFO,DUR3380,POS3090,STATUS2
// or #S2DISPLINE1,"1 of 17"
Matcher matcher = SRC_PATTERN.matcher(message);
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
// Amp controller sent 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
@ -349,7 +399,43 @@ public abstract class NuvoConnector {
return;
}
// Amp controller send a zone button press event ie: #Z11S3PLAYPAUSE
// Amp controller sent a zone BUTTONTWO press event ie: #Z11S3BUTTONTWO4,2,0,0,0
matcher = ZONE_BUTTONTWO_PATTERN.matcher(message);
if (matcher.find()) {
// redundant - ignore
return;
}
// Amp controller sent a zone BUTTON press event ie: #Z4S6BUTTON1,1,0xFFFFFFFF,1,3
matcher = ZONE_BUTTON2_PATTERN.matcher(message);
if (matcher.find()) {
// pull out the remainder of the message: button #, action, menuid, itemid, itemidx
String[] buttonSplit = matcher.group(3).split(COMMA);
// second field is button action, only send DOWNUP (0) or DOWN (1), ignore UP (2)
if (ZERO.equals(buttonSplit[1]) || ONE.equals(buttonSplit[1])) {
// a button in a menu was pressed, send SxZy,menuid,itemidx
if (!ZERO.equals(buttonSplit[2])) {
dispatchKeyValue(TYPE_MENU_ITEM_SELECTED, matcher.group(2), SRC_KEY + matcher.group(2) + ZONE_KEY
+ matcher.group(1) + COMMA + buttonSplit[2] + COMMA + buttonSplit[3]);
} else {
// send the button # in the event, don't send extra fields menuid, itemid, etc..
dispatchKeyValue(TYPE_ZONE_BUTTON2, matcher.group(2), buttonSplit[0]);
}
}
return;
}
// Amp controller sent a menu request event ie: #Z2S6MENUREQ0x0000000B,1,0,0
matcher = ZONE_MENUREQ_PATTERN.matcher(message);
if (matcher.find()) {
// pull out the source id and send SxZy plus the remainder of the message
dispatchKeyValue(TYPE_ZONE_MENUREQ, matcher.group(2),
SRC_KEY + matcher.group(2) + ZONE_KEY + matcher.group(1) + COMMA + matcher.group(3));
return;
}
// Amp controller sent 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
@ -357,7 +443,7 @@ public abstract class NuvoConnector {
return;
}
// Amp controller send a zone configuration response ie: #ZCFG1,BASS1,TREB-2,BALR2,LOUDCMP1
// Amp controller sent 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

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2022 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.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.imageio.ImageIO;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link NuvoImageResizer} class contains methods for re-sizing album art
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class NuvoImageResizer {
private static final String EXTENSION_JPG = "jpg";
private static final byte[] NO_IMAGE = { 0 };
public static byte[] resizeImage(byte[] inputImage, int width, int height) {
try {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(inputImage));
Image originalImage = image.getScaledInstance(width, height, Image.SCALE_DEFAULT);
int type = ((image.getType() == 0) ? BufferedImage.TYPE_INT_ARGB : image.getType());
BufferedImage resizedImage = new BufferedImage(width, height, type);
Graphics2D g2d = resizedImage.createGraphics();
g2d.setComposite(AlphaComposite.Src);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.drawImage(originalImage, 0, 0, width, height, null);
g2d.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(resizedImage, EXTENSION_JPG, baos);
return baos.toByteArray();
} catch (IOException e) {
return NO_IMAGE;
}
}
}

View File

@ -12,7 +12,8 @@
*/
package org.openhab.binding.nuvo.internal.communication;
import java.util.HashMap;
import static java.util.Map.entry;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -32,22 +33,21 @@ public class NuvoStatusCodes {
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", "Step Tune");
PLAY_MODE.put("10", "Seek Tune");
PLAY_MODE.put("11", "Preset Tune");
PLAY_MODE.put("12", "unknown-12");
}
public static final Map<String, String> PLAY_MODE = Map.ofEntries(entry("0", "Normal"), entry("1", "Idle"),
entry("2", "Playing"), entry("3", "Paused"), entry("4", "Fast Forward"), entry("5", "Rewind"),
entry("6", "Play Shuffle"), entry("7", "Play Repeat"), entry("8", "Play Shuffle Repeat"),
entry("9", "Step Tune"), entry("10", "Seek Tune"), entry("11", "Preset Tune"), entry("12", "unknown-12"));
// map to lookup button action name from NuvoNet button code
public static final Map<String, String> BUTTON_CODE = Map.ofEntries(entry("1", "OK"), entry("2", "PLAYPAUSE"),
entry("3", "PREV"), entry("4", "NEXT"), entry("5", "POWERMUTE"), // source will not receive this
entry("6", "UP"), // source will not receive this
entry("7", "DOWN"), // source will not receive this
entry("41", "DISCRETEPLAYPAUSE"), entry("42", "DISCRETENEXTTRACK"), entry("43", "DISCRETEPREVIOUSTRACK"),
entry("44", "SHUFFLETOGGLE"), entry("45", "REPEATTOGGLE"), entry("46", "TUNEUP"), entry("47", "TUNEDOWN"),
entry("48", "SEEKUP"), entry("49", "SEEKDOWN"), entry("50", "PRESETUP"), entry("51", "PRESETDOWN"),
entry("52", "DIRECTFREQUENCYENTRY"), entry("53", "DIRECTPRESETENTRY"), entry("54", "NEXTBAND"),
entry("55", "THUMBSUP"), entry("56", "THUMBSDOWN"));
/*
* This looks broken because the controller is seriously broken...

View File

@ -28,4 +28,29 @@ public class NuvoThingConfiguration {
public @Nullable Integer port;
public @Nullable Integer numZones;
public boolean clockSync;
public String favoriteLabels = "";
public Integer nuvoNetSrc1 = 0;
public Integer nuvoNetSrc2 = 0;
public Integer nuvoNetSrc3 = 0;
public Integer nuvoNetSrc4 = 0;
public Integer nuvoNetSrc5 = 0;
public Integer nuvoNetSrc6 = 0;
public String favoritesSrc1 = "";
public String favoritesSrc2 = "";
public String favoritesSrc3 = "";
public String favoritesSrc4 = "";
public String favoritesSrc5 = "";
public String favoritesSrc6 = "";
public String favPrefix1 = "";
public String favPrefix2 = "";
public String favPrefix3 = "";
public String favPrefix4 = "";
public String favPrefix5 = "";
public String favPrefix6 = "";
public String menuXmlSrc1 = "";
public String menuXmlSrc2 = "";
public String menuXmlSrc3 = "";
public String menuXmlSrc4 = "";
public String menuXmlSrc5 = "";
public String menuXmlSrc6 = "";
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2022 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.dto;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.stream.XMLInputFactory;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation for a static use of JAXBContext as singleton instance.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class JAXBUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(JAXBUtils.class);
public static final @Nullable JAXBContext JAXBCONTEXT_NUVO_MENU = initJAXBContextNuvoMenu();
public static final XMLInputFactory XMLINPUTFACTORY = initXMLInputFactory();
private static @Nullable JAXBContext initJAXBContextNuvoMenu() {
try {
return JAXBContext.newInstance(NuvoMenu.class);
} catch (JAXBException e) {
LOGGER.error("Exception creating JAXBContext for nuvo menu: {}", e.getLocalizedMessage(), e);
return null;
}
}
private static XMLInputFactory initXMLInputFactory() {
XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
return xif;
}
}

View File

@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2022 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.dto;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Create a Java object tree that represents the Nuvo keypad menu structure defined by the user
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "")
@XmlRootElement(name = "menu")
public class NuvoMenu {
@XmlElement(required = true)
protected List<NuvoMenu.Source> source = new ArrayList<NuvoMenu.Source>();
public List<NuvoMenu.Source> getSource() {
return this.source;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "")
public static class Source {
protected List<NuvoMenu.Source.TopMenu> topmenu = new ArrayList<NuvoMenu.Source.TopMenu>();
public List<NuvoMenu.Source.TopMenu> getTopMenu() {
return this.topmenu;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "")
public static class TopMenu {
protected List<String> item = new ArrayList<String>();
@XmlAttribute(name = "text", required = true)
protected String text = "";
public List<String> getItems() {
return this.item;
}
public String getText() {
return text;
}
public void setText(String value) {
this.text = value;
}
}
}
}

View File

@ -12,20 +12,27 @@
*/
package org.openhab.binding.nuvo.internal.handler;
import static org.eclipse.jetty.http.HttpMethod.GET;
import static org.eclipse.jetty.http.HttpStatus.OK_200;
import static org.openhab.binding.nuvo.internal.NuvoBindingConstants.*;
import java.io.StringReader;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -33,9 +40,16 @@ import java.util.stream.IntStream;
import javax.measure.Unit;
import javax.measure.quantity.Time;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.openhab.binding.nuvo.internal.NuvoException;
import org.openhab.binding.nuvo.internal.NuvoStateDescriptionOptionProvider;
import org.openhab.binding.nuvo.internal.NuvoThingActions;
@ -43,12 +57,16 @@ 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.NuvoImageResizer;
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.binding.nuvo.internal.dto.JAXBUtils;
import org.openhab.binding.nuvo.internal.dto.NuvoMenu;
import org.openhab.binding.nuvo.internal.dto.NuvoMenu.Source.TopMenu;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.NextPreviousType;
@ -108,6 +126,8 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
private static final int MPS4_PORT = 5006;
private static final byte[] NO_ART = { 0 };
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}),\"(.*)\"$");
@ -115,11 +135,10 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
.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 final HttpClient httpClient;
private @Nullable ScheduledFuture<?> reconnectJob;
private @Nullable ScheduledFuture<?> pollingJob;
@ -133,6 +152,16 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
private boolean isGConcerto = false;
private Object sequenceLock = new Object();
private boolean isAnyOhNuvoNet = false;
private NuvoMenu nuvoMenus = new NuvoMenu();
private HashMap<String, Integer> nuvoNetSrcMap = new HashMap<String, Integer>();
private HashMap<String, String> favPrefixMap = new HashMap<String, String>();
private HashMap<String, String[]> favoriteMap = new HashMap<String, String[]>();
private HashMap<String, byte[]> albumArtMap = new HashMap<String, byte[]>();
private HashMap<String, Integer> albumArtIds = new HashMap<String, Integer>();
private HashMap<String, String> dispInfoCache = new HashMap<String, String>();
Set<Integer> activeZones = new HashSet<>(1);
// A tree map that maps the source ids to source labels
@ -146,10 +175,11 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
* Constructor
*/
public NuvoHandler(Thing thing, NuvoStateDescriptionOptionProvider stateDescriptionProvider,
SerialPortManager serialPortManager) {
SerialPortManager serialPortManager, HttpClient httpClient) {
super(thing);
this.stateDescriptionProvider = stateDescriptionProvider;
this.serialPortManager = serialPortManager;
this.httpClient = httpClient;
}
@Override
@ -187,15 +217,65 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
} else if (port != null) {
connector = new NuvoIpConnector(host, port, uid);
this.isMps4 = (port.intValue() == MPS4_PORT);
if (this.isMps4) {
logger.debug("Port set to {} configuring binding for MPS4 compatability", MPS4_PORT);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Either Serial port or Host & Port must be specifed");
return;
}
if (this.isMps4) {
logger.debug("Port set to {} configuring binding for MPS4 compatability", MPS4_PORT);
this.isAnyOhNuvoNet = (config.nuvoNetSrc1 == 2 || config.nuvoNetSrc2 == 2 || config.nuvoNetSrc3 == 2
|| config.nuvoNetSrc4 == 2 || config.nuvoNetSrc5 == 2 || config.nuvoNetSrc6 == 2);
if (this.isAnyOhNuvoNet) {
logger.debug("At least one source is configured as an openHAB NuvoNet source");
loadMenuConfiguration(config);
nuvoNetSrcMap.put("1", config.nuvoNetSrc1);
nuvoNetSrcMap.put("2", config.nuvoNetSrc2);
nuvoNetSrcMap.put("3", config.nuvoNetSrc3);
nuvoNetSrcMap.put("4", config.nuvoNetSrc4);
nuvoNetSrcMap.put("5", config.nuvoNetSrc5);
nuvoNetSrcMap.put("6", config.nuvoNetSrc6);
favoriteMap.put("1",
!config.favoritesSrc1.isEmpty() ? config.favoritesSrc1.split(COMMA) : new String[0]);
favoriteMap.put("2",
!config.favoritesSrc2.isEmpty() ? config.favoritesSrc2.split(COMMA) : new String[0]);
favoriteMap.put("3",
!config.favoritesSrc3.isEmpty() ? config.favoritesSrc3.split(COMMA) : new String[0]);
favoriteMap.put("4",
!config.favoritesSrc4.isEmpty() ? config.favoritesSrc4.split(COMMA) : new String[0]);
favoriteMap.put("5",
!config.favoritesSrc5.isEmpty() ? config.favoritesSrc5.split(COMMA) : new String[0]);
favoriteMap.put("6",
!config.favoritesSrc6.isEmpty() ? config.favoritesSrc6.split(COMMA) : new String[0]);
favPrefixMap.put("1", config.favPrefix1);
favPrefixMap.put("2", config.favPrefix2);
favPrefixMap.put("3", config.favPrefix3);
favPrefixMap.put("4", config.favPrefix4);
favPrefixMap.put("5", config.favPrefix5);
favPrefixMap.put("6", config.favPrefix6);
albumArtIds.put("S1", 0);
albumArtIds.put("S2", 0);
albumArtIds.put("S3", 0);
albumArtIds.put("S4", 0);
albumArtIds.put("S5", 0);
albumArtIds.put("S6", 0);
albumArtMap.put("S1", NO_ART);
albumArtMap.put("S2", NO_ART);
albumArtMap.put("S3", NO_ART);
albumArtMap.put("S4", NO_ART);
albumArtMap.put("S5", NO_ART);
albumArtMap.put("S6", NO_ART);
}
}
if (numZones != null) {
this.numZones = numZones;
}
@ -213,6 +293,25 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
updateThing(editThing().withChannels(channels).build());
}
// Build a list of State options for the global favorites using user config values (if supplied)
String[] favoritesArr = !config.favoriteLabels.isEmpty() ? config.favoriteLabels.split(COMMA) : new String[0];
List<StateOption> favoriteLabelsStateOptions = new ArrayList<>();
for (int i = 0; i < 12; i++) {
if (favoritesArr.length > i) {
favoriteLabelsStateOptions.add(new StateOption(String.valueOf(i + 1), favoritesArr[i]));
} else if (favoritesArr.length == 0) {
favoriteLabelsStateOptions.add(new StateOption(String.valueOf(i + 1), "Favorite " + (i + 1)));
}
}
// Put the global favorites labels on all active zones
activeZones.forEach(zoneNum -> {
stateDescriptionProvider.setStateOptions(
new ChannelUID(getThing().getUID(),
ZONE.toLowerCase() + zoneNum + CHANNEL_DELIMIT + CHANNEL_TYPE_FAVORITE),
favoriteLabelsStateOptions);
});
if (config.clockSync) {
scheduleClockSyncJob();
}
@ -225,6 +324,37 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
@Override
public void dispose() {
if (this.isAnyOhNuvoNet) {
try {
// disable NuvoNet for each source that was configured as an openHAB NuvoNet source
nuvoNetSrcMap.forEach((srcNum, val) -> {
if (val == 2) {
try {
connector.sendCommand(SRC_KEY + srcNum + "DISPINFOTWO0,0,0,0,0,0,0");
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
connector.sendCommand(
SRC_KEY + srcNum + "DISPLINES0,0,0,\"Source Unavailable\",\"\",\"\",\"\"");
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
connector.sendCommand("SCFG" + srcNum + "NUVONET0");
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
} catch (NuvoException | InterruptedException e) {
logger.debug("Error sending command to disable NuvoNet source: {}", srcNum);
}
}
});
// need '1' flag for sources configured as an MPS4 NuvoNet source, but disable openHAB NuvoNet sources
connector.sendCommand("SNUMBERS" + (nuvoNetSrcMap.get("1") == 1 ? ONE : ZERO) + COMMA
+ (nuvoNetSrcMap.get("2") == 1 ? ONE : ZERO) + COMMA
+ (nuvoNetSrcMap.get("3") == 1 ? ONE : ZERO) + COMMA
+ (nuvoNetSrcMap.get("4") == 1 ? ONE : ZERO) + COMMA
+ (nuvoNetSrcMap.get("5") == 1 ? ONE : ZERO) + COMMA
+ (nuvoNetSrcMap.get("6") == 1 ? ONE : ZERO));
} catch (NuvoException e) {
logger.debug("Error sending SNUMBERS command to disable NuvoNet sources");
}
}
cancelReconnectJob();
cancelPollingJob();
cancelClockSyncJob();
@ -249,7 +379,7 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
}
/**
* Handle a command the UI
* Handle a command from the UI
*
* @param channelUID the channel sending the command
* @param command the command received
@ -410,6 +540,54 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
connector.sendCommand(command == OnOffType.ON ? NuvoCommand.PAGE_ON : NuvoCommand.PAGE_OFF);
}
break;
case CHANNEL_TYPE_SENDCMD:
if (command instanceof StringType) {
String commandStr = command.toString();
if (commandStr.contains(DISP_INFO_TWO)) {
String sourceKey = commandStr.split(DISP_INFO_TWO)[0];
dispInfoCache.put(sourceKey, commandStr);
// if 'albumartid' is present, substitute it with the albumArtId hex string
connector.sendCommand(commandStr.replace(ALBUM_ART_ID,
(OFFSET_ZERO + Integer.toHexString(albumArtIds.get(sourceKey)))));
} else {
connector.sendCommand(commandStr);
}
}
break;
case CHANNEL_ART_URL:
if (command instanceof StringType) {
String url = command.toString();
if (url.startsWith(HTTP) || url.startsWith(HTTPS)) {
try {
ContentResponse contentResponse = httpClient.newRequest(url).method(GET)
.timeout(10, TimeUnit.SECONDS).send();
int httpStatus = contentResponse.getStatus();
if (httpStatus == OK_200) {
albumArtMap.put(target.getId(),
NuvoImageResizer.resizeImage(contentResponse.getContent(), 80, 80));
} else {
albumArtMap.put(target.getId(), NO_ART);
albumArtIds.put(target.getId(), 0);
return;
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
albumArtMap.put(target.getId(), NO_ART);
albumArtIds.put(target.getId(), 0);
return;
}
albumArtIds.put(target.getId(), Math.abs(url.hashCode()));
// re-send the cached DISPINFOTWO message, substituting in the new albumArtId
if (dispInfoCache.get(target.getId()) != null) {
connector.sendCommand(dispInfoCache.get(target.getId()).replace(ALBUM_ART_ID,
(OFFSET_ZERO + Integer.toHexString(albumArtIds.get(target.getId())))));
}
} else {
albumArtMap.put(target.getId(), NO_ART);
albumArtIds.put(target.getId(), 0);
}
}
}
} catch (NuvoException e) {
logger.warn("Command {} from channel {} failed: {}", command, channel, e.getMessage());
@ -477,6 +655,10 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
logger.debug("Grand Concerto not detected");
}
break;
case TYPE_RESTART:
logger.debug("Restart message received; re-sending initialization messages");
enableNuvonet(false);
return;
case TYPE_PING:
logger.debug("Ping message received- rescheduling ping timeout");
schedulePingTimeoutJob();
@ -560,6 +742,85 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
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_BUTTON2:
String buttonAction = NuvoStatusCodes.BUTTON_CODE.get(updateData);
if (buttonAction != null) {
logger.debug("Zone NuvoNet Button pressed: Source: {} - Button: {}", key, buttonAction);
updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, buttonAction);
} else {
logger.debug("Zone NuvoNet Button pressed: Source: {} - Unknown button code: {}", key, updateData);
updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, updateData);
}
break;
case TYPE_MENU_ITEM_SELECTED:
String[] updateDataSplit = updateData.split(COMMA);
String zoneSource = updateDataSplit[0];
String menuId = updateDataSplit[1];
int menuItemIdx = Integer.parseInt(updateDataSplit[2]) - 1;
boolean exitMenu = false;
if ("0xFFFFFFFF".equals(menuId)) {
TopMenu topMenuItem = nuvoMenus.getSource().get(Integer.parseInt(key) - 1).getTopMenu()
.get(menuItemIdx);
logger.debug("Top Menu item selected: Source: {} - Menu Item: {}", key, topMenuItem.getText());
updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, topMenuItem.getText());
List<String> subMenuItems = topMenuItem.getItems();
if (subMenuItems.isEmpty()) {
exitMenu = true;
} else {
// send submenu (maximum of 20 items)
int subMenuSize = subMenuItems.size() < 20 ? subMenuItems.size() : 20;
try {
connector.sendCommand(zoneSource + "MENU" + (menuItemIdx + 11) + ",0,0," + subMenuSize
+ ",0,0," + subMenuSize + ",\"" + topMenuItem.getText() + "\"");
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
for (int i = 0; i < subMenuSize; i++) {
connector.sendCommand(
zoneSource + "MENUITEM" + (i + 1) + ",0,0,\"" + subMenuItems.get(i) + "\"");
}
} catch (NuvoException | InterruptedException e) {
logger.debug("Error sending sub menu for {}", zoneSource);
}
}
} else {
// a sub menu item was selected
TopMenu topMenuItem = nuvoMenus.getSource().get(Integer.parseInt(key) - 1).getTopMenu()
.get(Integer.decode(menuId) - 11);
String subMenuItem = topMenuItem.getItems().get(menuItemIdx);
logger.debug("Sub Menu item selected: Source: {} - Menu Item: {}", key,
topMenuItem.getText() + "|" + subMenuItem);
updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS,
topMenuItem.getText() + "|" + subMenuItem);
exitMenu = true;
}
if (exitMenu) {
try {
// tell the zone to exit the menu
connector.sendCommand(zoneSource + "MENU0,0,0,0,0,0,0,\"\"");
} catch (NuvoException e) {
logger.debug("Error sending exit menu command for {}", zoneSource);
}
}
break;
case TYPE_ZONE_MENUREQ:
logger.debug("Menu Request: Source: {} - Value: {}", key, updateData);
// For now we only support one level deep menus. If third field is '1', indicates go back to main menu.
String[] menuDataSplit = updateData.split(",");
if (menuDataSplit.length > 3 && ONE.equals(menuDataSplit[2])) {
try {
connector.sendCommand(menuDataSplit[0] + "MENU0xFFFFFFFF,0,0,0,0,0,0,\"\"");
} catch (NuvoException e) {
logger.debug("Error sending main menu command for {}", menuDataSplit[0]);
}
}
break;
case TYPE_ZONE_CONFIG:
logger.debug("Zone Configuration: Zone: {} - Value: {}", key, updateData);
@ -576,6 +837,58 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
logger.debug("no match on message: {}", updateData);
}
break;
case TYPE_ALBUM_ART_REQ:
logger.debug("Album Art Request for Source: {} - Data: {}", key, updateData);
// 0x620FD879,80,80,2,0x00C0C0C0,0,0,0,0,1
String[] albumArtReq = updateData.split(COMMA);
albumArtIds.put(SRC_KEY + key, Integer.decode(albumArtReq[0]));
try {
if (albumArtMap.get(SRC_KEY + key).length > 1) {
connector.sendCommand(SRC_KEY + key + ALBUM_ART_AVAILABLE + albumArtIds.get(SRC_KEY + key)
+ COMMA + albumArtMap.get(SRC_KEY + key).length);
} else {
connector.sendCommand(SRC_KEY + key + ALBUM_ART_AVAILABLE + ZERO_COMMA);
}
} catch (NuvoException e) {
logger.debug("Error sending ALBUMARTAVAILABLE command for source: {}", key);
}
break;
case TYPE_ALBUM_ART_FRAG_REQ:
logger.debug("Album Art Fragment Request for Source: {} - Data: {}", key, updateData);
// 0x620FD879,0,750 (id, requested offset from start of image, byte length requested)
String[] albumArtFragReq = updateData.split(COMMA);
int requestedId = Integer.decode(albumArtFragReq[0]);
int offset = Integer.parseInt(albumArtFragReq[1]);
int length = Integer.parseInt(albumArtFragReq[2]);
if (requestedId == albumArtIds.get(SRC_KEY + key)) {
byte[] chunk = new byte[length];
byte[] albumArtBytes = albumArtMap.get(SRC_KEY + key);
if (albumArtBytes != null) {
System.arraycopy(albumArtBytes, offset, chunk, 0, length);
final String frag = Base64.getEncoder().encodeToString(chunk);
try {
connector.sendCommand(SRC_KEY + key + ALBUM_ART_FRAG + requestedId + COMMA + offset + COMMA
+ frag.length() + COMMA + frag);
} catch (NuvoException e) {
logger.debug("Error sending ALBUMARTFRAG command for source: {}, artId: {}", key,
requestedId);
}
}
}
break;
case TYPE_FAVORITE_REQ:
logger.debug("Favorite request for source: {} - favoriteId: {}", key, updateData);
try {
int playlistIdx = Integer.parseInt(updateData, 16) - 1000;
updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS,
"PLAY_MUSIC_PRESET:" + favoriteMap.get(key)[playlistIdx]);
} catch (NumberFormatException nfe) {
logger.debug("Unable to parse favoriteId: {}", updateData);
}
break;
default:
logger.debug("onNewMessageEvent: unhandled key {}", key);
// Return here because receiving an unknown message does not indicate that one can poll
@ -587,6 +900,138 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
}
}
private void loadMenuConfiguration(NuvoThingConfiguration config) {
StringBuilder menuXml = new StringBuilder("<menu>");
if (!config.menuXmlSrc1.isEmpty()) {
menuXml.append("<source>" + config.menuXmlSrc1 + "</source>");
} else {
menuXml.append("<source/>");
}
if (!config.menuXmlSrc2.isEmpty()) {
menuXml.append("<source>" + config.menuXmlSrc2 + "</source>");
} else {
menuXml.append("<source/>");
}
if (!config.menuXmlSrc3.isEmpty()) {
menuXml.append("<source>" + config.menuXmlSrc3 + "</source>");
} else {
menuXml.append("<source/>");
}
if (!config.menuXmlSrc4.isEmpty()) {
menuXml.append("<source>" + config.menuXmlSrc4 + "</source>");
} else {
menuXml.append("<source/>");
}
if (!config.menuXmlSrc5.isEmpty()) {
menuXml.append("<source>" + config.menuXmlSrc5 + "</source>");
} else {
menuXml.append("<source/>");
}
if (!config.menuXmlSrc6.isEmpty()) {
menuXml.append("<source>" + config.menuXmlSrc6 + "</source>");
} else {
menuXml.append("<source/>");
}
menuXml.append("</menu>");
try {
JAXBContext ctx = JAXBUtils.JAXBCONTEXT_NUVO_MENU;
if (ctx != null) {
Unmarshaller unmarshaller = ctx.createUnmarshaller();
if (unmarshaller != null) {
XMLStreamReader xsr = JAXBUtils.XMLINPUTFACTORY
.createXMLStreamReader(new StringReader(menuXml.toString()));
NuvoMenu menu = (NuvoMenu) unmarshaller.unmarshal(xsr);
if (menu != null) {
nuvoMenus = menu;
return;
}
}
}
logger.debug("No JAXBContext available to parse Nuvo Menu XML");
} catch (JAXBException | XMLStreamException e) {
logger.warn("Error processing Nuvo Menu XML: {}", e.getLocalizedMessage());
}
}
private void enableNuvonet(boolean showReady) {
if (!this.isAnyOhNuvoNet) {
return;
}
// enable NuvoNet for each source configured as an openHAB NuvoNet source
nuvoNetSrcMap.forEach((srcNum, val) -> {
if (val == 2) {
try {
connector.sendCommand("SCFG" + srcNum + "NUVONET1");
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
} catch (NuvoException | InterruptedException e) {
logger.debug("Error sending SCFG command for source: {}", srcNum);
}
}
});
try {
// set '1' flag for each source configured as an MPS4 NuvoNet source or openHAB NuvoNet source
connector.sendCommand("SNUMBERS" + (nuvoNetSrcMap.get("1") > 0 ? ONE : ZERO) + COMMA
+ (nuvoNetSrcMap.get("2") > 0 ? ONE : ZERO) + COMMA + (nuvoNetSrcMap.get("3") > 0 ? ONE : ZERO)
+ COMMA + (nuvoNetSrcMap.get("4") > 0 ? ONE : ZERO) + COMMA
+ (nuvoNetSrcMap.get("5") > 0 ? ONE : ZERO) + COMMA + (nuvoNetSrcMap.get("6") > 0 ? ONE : ZERO));
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
} catch (NuvoException | InterruptedException e) {
logger.debug("Error sending SNUMBERS command");
}
// go though each source and if is openHAB NuvoNet then configure menu, favorites, etc.
nuvoNetSrcMap.forEach((srcNum, val) -> {
if (val == 2) {
try {
List<TopMenu> topMenuItems = nuvoMenus.getSource().get(Integer.parseInt(srcNum) - 1).getTopMenu();
if (!topMenuItems.isEmpty()) {
connector.sendCommand(
SRC_KEY + srcNum + "MENU," + (topMenuItems.size() < 10 ? topMenuItems.size() : 10));
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
for (int i = 0; i < (topMenuItems.size() < 10 ? topMenuItems.size() : 10); i++) {
connector.sendCommand(SRC_KEY + srcNum + "MENUITEM" + (i + 1) + ","
+ (topMenuItems.get(i).getItems().isEmpty() ? ZERO : ONE) + ",0,\""
+ topMenuItems.get(i).getText() + "\"");
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
}
}
String[] favorites = favoriteMap.get(srcNum);
if (favorites != null) {
connector.sendCommand(SRC_KEY + srcNum + "FAVORITES"
+ (favorites.length < 20 ? favorites.length : 20) + COMMA
+ ("1".equals(srcNum) ? ONE : ZERO) + COMMA + ("2".equals(srcNum) ? ONE : ZERO) + COMMA
+ ("3".equals(srcNum) ? ONE : ZERO) + COMMA + ("4".equals(srcNum) ? ONE : ZERO) + COMMA
+ ("5".equals(srcNum) ? ONE : ZERO) + COMMA + ("6".equals(srcNum) ? ONE : ZERO));
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
for (int i = 0; i < (favorites.length < 20 ? favorites.length : 20); i++) {
connector.sendCommand(SRC_KEY + srcNum + "FAVORITESITEM" + (i + 1000) + ",0,0,\""
+ favPrefixMap.get(srcNum) + favorites[i] + "\"");
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
}
}
if (showReady) {
connector.sendCommand(SRC_KEY + srcNum + "DISPINFOTWO0,0,0,0,0,0,0");
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
connector.sendCommand(SRC_KEY + srcNum + "DISPLINES0,0,0,\"Ready\",\"\",\"\",\"\"");
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
}
} catch (NuvoException | InterruptedException e) {
logger.debug("Error configuring NuvoNet for source: {}", srcNum);
}
}
});
}
/**
* Schedule the reconnection job
*/
@ -603,6 +1048,7 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
if (!isMps4) {
pollStatus();
}
enableNuvonet(true);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Reconnection failed");
closeConnection();
@ -760,7 +1206,8 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
clockSyncJob = scheduler.scheduleWithFixedDelay(() -> {
if (this.isGConcerto) {
try {
connector.sendCommand(NuvoCommand.CFGTIME.getValue() + DATE_FORMAT.format(new Date()));
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy,MM,dd,HH,mm");
connector.sendCommand(NuvoCommand.CFGTIME.getValue() + dateFormat.format(new Date()));
} catch (NuvoException e) {
logger.debug("Error syncing clock: {}", e.getMessage());
}

View File

@ -66,10 +66,78 @@ thing-type.nuvo.amplifier.group.zone20.description = The Controls for Zone 20
thing-type.config.nuvo.amplifier.clockSync.label = Sync Clock On GConcerto
thing-type.config.nuvo.amplifier.clockSync.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.
thing-type.config.nuvo.amplifier.favPrefix1.label = S1 Favorite Prefix
thing-type.config.nuvo.amplifier.favPrefix1.description = To quickly locate Source 1's favorites, this prefix will be added to the favorite names
thing-type.config.nuvo.amplifier.favPrefix2.label = S2 Favorite Prefix
thing-type.config.nuvo.amplifier.favPrefix2.description = To quickly locate Source 2's favorites, this prefix will be added to the favorite names
thing-type.config.nuvo.amplifier.favPrefix3.label = S3 Favorite Prefix
thing-type.config.nuvo.amplifier.favPrefix3.description = To quickly locate Source 3's favorites, this prefix will be added to the favorite names
thing-type.config.nuvo.amplifier.favPrefix4.label = S4 Favorite Prefix
thing-type.config.nuvo.amplifier.favPrefix4.description = To quickly locate Source 4's favorites, this prefix will be added to the favorite names
thing-type.config.nuvo.amplifier.favPrefix5.label = S5 Favorite Prefix
thing-type.config.nuvo.amplifier.favPrefix5.description = To quickly locate Source 5's favorites, this prefix will be added to the favorite names
thing-type.config.nuvo.amplifier.favPrefix6.label = S6 Favorite Prefix
thing-type.config.nuvo.amplifier.favPrefix6.description = To quickly locate Source 6's favorites, this prefix will be added to the favorite names
thing-type.config.nuvo.amplifier.favoriteLabels.label = Favorite Labels
thing-type.config.nuvo.amplifier.favoriteLabels.description = A comma separated list of up to 12 label names that are loaded into the 'favorites' channel of each zone
thing-type.config.nuvo.amplifier.favoritesSrc1.label = S1 Favorites
thing-type.config.nuvo.amplifier.favoritesSrc1.description = A comma separated list of favorite names to load into Source 1
thing-type.config.nuvo.amplifier.favoritesSrc2.label = S2 Favorites
thing-type.config.nuvo.amplifier.favoritesSrc2.description = A comma separated list of favorite names to load into Source 2
thing-type.config.nuvo.amplifier.favoritesSrc3.label = S3 Favorites
thing-type.config.nuvo.amplifier.favoritesSrc3.description = A comma separated list of favorite names to load into Source 3
thing-type.config.nuvo.amplifier.favoritesSrc4.label = S4 Favorites
thing-type.config.nuvo.amplifier.favoritesSrc4.description = A comma separated list of favorite names to load into Source 4
thing-type.config.nuvo.amplifier.favoritesSrc5.label = S5 Favorites
thing-type.config.nuvo.amplifier.favoritesSrc5.description = A comma separated list of favorite names to load into Source 5
thing-type.config.nuvo.amplifier.favoritesSrc6.label = S6 Favorites
thing-type.config.nuvo.amplifier.favoritesSrc6.description = A comma separated list of favorite names to load into Source 6
thing-type.config.nuvo.amplifier.host.label = Address
thing-type.config.nuvo.amplifier.host.description = Host Name or IP Address of the machine connected to the Nuvo amplifier (Serial over IP)
thing-type.config.nuvo.amplifier.menuXmlSrc1.label = S1 Menu XML
thing-type.config.nuvo.amplifier.menuXmlSrc1.description = An XML string representing the menu items to load into the keypad for Source 1, see README
thing-type.config.nuvo.amplifier.menuXmlSrc2.label = S2 Menu XML
thing-type.config.nuvo.amplifier.menuXmlSrc2.description = An XML string representing the menu items to load into the keypad for Source 2, see README
thing-type.config.nuvo.amplifier.menuXmlSrc3.label = S3 Menu XML
thing-type.config.nuvo.amplifier.menuXmlSrc3.description = An XML string representing the menu items to load into the keypad for Source 3, see README
thing-type.config.nuvo.amplifier.menuXmlSrc4.label = S4 Menu XML
thing-type.config.nuvo.amplifier.menuXmlSrc4.description = An XML string representing the menu items to load into the keypad for Source 4, see README
thing-type.config.nuvo.amplifier.menuXmlSrc5.label = S5 Menu XML
thing-type.config.nuvo.amplifier.menuXmlSrc5.description = An XML string representing the menu items to load into the keypad for Source 5, see README
thing-type.config.nuvo.amplifier.menuXmlSrc6.label = S6 Menu XML
thing-type.config.nuvo.amplifier.menuXmlSrc6.description = An XML string representing the menu items to load into the keypad for Source 6, see README
thing-type.config.nuvo.amplifier.numZones.label = Number of Zones
thing-type.config.nuvo.amplifier.numZones.description = Number of Zones on the amplifier to utilize in the binding (Up to 20 zones when using expansion module)
thing-type.config.nuvo.amplifier.nuvoNetSrc1.label = S1 is NuvoNet
thing-type.config.nuvo.amplifier.nuvoNetSrc1.description = Indicates if Source 1 is configured as a NuvoNet source
thing-type.config.nuvo.amplifier.nuvoNetSrc1.option.0 = No
thing-type.config.nuvo.amplifier.nuvoNetSrc1.option.1 = MPS4 NuvoNet Source
thing-type.config.nuvo.amplifier.nuvoNetSrc1.option.2 = openHAB NuvoNet Source
thing-type.config.nuvo.amplifier.nuvoNetSrc2.label = S2 is NuvoNet
thing-type.config.nuvo.amplifier.nuvoNetSrc2.description = Indicates if Source 2 is configured as a NuvoNet source
thing-type.config.nuvo.amplifier.nuvoNetSrc2.option.0 = No
thing-type.config.nuvo.amplifier.nuvoNetSrc2.option.1 = MPS4 NuvoNet Source
thing-type.config.nuvo.amplifier.nuvoNetSrc2.option.2 = openHAB NuvoNet Source
thing-type.config.nuvo.amplifier.nuvoNetSrc3.label = S3 is NuvoNet
thing-type.config.nuvo.amplifier.nuvoNetSrc3.description = Indicates if Source 3 is configured as a NuvoNet source
thing-type.config.nuvo.amplifier.nuvoNetSrc3.option.0 = No
thing-type.config.nuvo.amplifier.nuvoNetSrc3.option.1 = MPS4 NuvoNet Source
thing-type.config.nuvo.amplifier.nuvoNetSrc3.option.2 = openHAB NuvoNet Source
thing-type.config.nuvo.amplifier.nuvoNetSrc4.label = S4 is NuvoNet
thing-type.config.nuvo.amplifier.nuvoNetSrc4.description = Indicates if Source 4 is configured as a NuvoNet source
thing-type.config.nuvo.amplifier.nuvoNetSrc4.option.0 = No
thing-type.config.nuvo.amplifier.nuvoNetSrc4.option.1 = MPS4 NuvoNet Source
thing-type.config.nuvo.amplifier.nuvoNetSrc4.option.2 = openHAB NuvoNet Source
thing-type.config.nuvo.amplifier.nuvoNetSrc5.label = S5 is NuvoNet
thing-type.config.nuvo.amplifier.nuvoNetSrc5.description = Indicates if Source 5 is configured as a NuvoNet source
thing-type.config.nuvo.amplifier.nuvoNetSrc5.option.0 = No
thing-type.config.nuvo.amplifier.nuvoNetSrc5.option.1 = MPS4 NuvoNet Source
thing-type.config.nuvo.amplifier.nuvoNetSrc5.option.2 = openHAB NuvoNet Source
thing-type.config.nuvo.amplifier.nuvoNetSrc6.label = S6 is NuvoNet
thing-type.config.nuvo.amplifier.nuvoNetSrc6.description = Indicates if Source 6 is configured as a NuvoNet source
thing-type.config.nuvo.amplifier.nuvoNetSrc6.option.0 = No
thing-type.config.nuvo.amplifier.nuvoNetSrc6.option.1 = MPS4 NuvoNet Source
thing-type.config.nuvo.amplifier.nuvoNetSrc6.option.2 = openHAB NuvoNet Source
thing-type.config.nuvo.amplifier.port.label = Port
thing-type.config.nuvo.amplifier.port.description = Communication Port (serial over IP). For IP connection to the Nuvo amplifier. Use port 5006 with MPS4 server.
thing-type.config.nuvo.amplifier.serialPort.label = Serial Port
@ -88,6 +156,8 @@ channel-group-type.nuvo.zone.description = The Controls for the Zone
channel-type.nuvo.alloff.label = All Off
channel-type.nuvo.alloff.description = Turn All Zones Off
channel-type.nuvo.art_url.label = Album Art URL
channel-type.nuvo.art_url.description = The URL of the Album Art JPG for this source that is displayed on a CTP-36
channel-type.nuvo.balance.label = Balance Adjustment
channel-type.nuvo.balance.description = Adjust the Balance Setting for the Zone
channel-type.nuvo.bass.label = Bass Adjustment
@ -108,18 +178,6 @@ channel-type.nuvo.dnd.label = Do Not Disturb
channel-type.nuvo.dnd.description = A Switch That Controls If the Zone Should Ignore an Incoming Audio Page
channel-type.nuvo.favorite.label = Favorite
channel-type.nuvo.favorite.description = Select a Preset Favorite for the Zone
channel-type.nuvo.favorite.state.option.1 = Favorite 1
channel-type.nuvo.favorite.state.option.2 = Favorite 2
channel-type.nuvo.favorite.state.option.3 = Favorite 3
channel-type.nuvo.favorite.state.option.4 = Favorite 4
channel-type.nuvo.favorite.state.option.5 = Favorite 5
channel-type.nuvo.favorite.state.option.6 = Favorite 6
channel-type.nuvo.favorite.state.option.7 = Favorite 7
channel-type.nuvo.favorite.state.option.8 = Favorite 8
channel-type.nuvo.favorite.state.option.9 = Favorite 9
channel-type.nuvo.favorite.state.option.10 = Favorite 10
channel-type.nuvo.favorite.state.option.11 = Favorite 11
channel-type.nuvo.favorite.state.option.12 = Favorite 12
channel-type.nuvo.lock.label = Locked
channel-type.nuvo.lock.description = Indicates If This Zone Is Locked
channel-type.nuvo.lock.state.option.CLOSED = Unlocked
@ -132,6 +190,8 @@ channel-type.nuvo.party.label = Party Mode
channel-type.nuvo.party.description = Activate Party Mode With This Zone as the Host
channel-type.nuvo.play_mode.label = Play Mode
channel-type.nuvo.play_mode.description = The Current Playback Mode of the Source
channel-type.nuvo.sendcmd.label = Send Command
channel-type.nuvo.sendcmd.description = Send a command to the amplifier
channel-type.nuvo.source.label = Source Input
channel-type.nuvo.source.description = Select the Source Input for the Zone
channel-type.nuvo.track_length.label = Track Length

View File

@ -145,6 +145,11 @@
<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="favoriteLabels" type="text" required="false">
<label>Favorite Labels</label>
<description>A comma separated list of up to 12 label names that are loaded into the 'favorites' channel of each
zone</description>
</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
@ -152,6 +157,180 @@
so this setting has no effect on that component.</description>
<default>false</default>
</parameter>
<parameter name="nuvoNetSrc1" type="integer" required="true">
<label>S1 is NuvoNet</label>
<description>Indicates if Source 1 is configured as a NuvoNet source</description>
<limitToOptions>true</limitToOptions>
<options>
<option value="0">No</option>
<option value="1">MPS4 NuvoNet Source</option>
<option value="2">openHAB NuvoNet Source</option>
</options>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="nuvoNetSrc2" type="integer" required="true">
<label>S2 is NuvoNet</label>
<description>Indicates if Source 2 is configured as a NuvoNet source</description>
<limitToOptions>true</limitToOptions>
<options>
<option value="0">No</option>
<option value="1">MPS4 NuvoNet Source</option>
<option value="2">openHAB NuvoNet Source</option>
</options>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="nuvoNetSrc3" type="integer" required="true">
<label>S3 is NuvoNet</label>
<description>Indicates if Source 3 is configured as a NuvoNet source</description>
<limitToOptions>true</limitToOptions>
<options>
<option value="0">No</option>
<option value="1">MPS4 NuvoNet Source</option>
<option value="2">openHAB NuvoNet Source</option>
</options>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="nuvoNetSrc4" type="integer" required="true">
<label>S4 is NuvoNet</label>
<description>Indicates if Source 4 is configured as a NuvoNet source</description>
<limitToOptions>true</limitToOptions>
<options>
<option value="0">No</option>
<option value="1">MPS4 NuvoNet Source</option>
<option value="2">openHAB NuvoNet Source</option>
</options>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="nuvoNetSrc5" type="integer" required="true">
<label>S5 is NuvoNet</label>
<description>Indicates if Source 5 is configured as a NuvoNet source</description>
<limitToOptions>true</limitToOptions>
<options>
<option value="0">No</option>
<option value="1">MPS4 NuvoNet Source</option>
<option value="2">openHAB NuvoNet Source</option>
</options>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="nuvoNetSrc6" type="integer" required="true">
<label>S6 is NuvoNet</label>
<description>Indicates if Source 6 is configured as a NuvoNet source</description>
<limitToOptions>true</limitToOptions>
<options>
<option value="0">No</option>
<option value="1">MPS4 NuvoNet Source</option>
<option value="2">openHAB NuvoNet Source</option>
</options>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="favoritesSrc1" type="text" required="false">
<label>S1 Favorites</label>
<description>A comma separated list of favorite names to load into Source 1</description>
<advanced>true</advanced>
</parameter>
<parameter name="favoritesSrc2" type="text" required="false">
<label>S2 Favorites</label>
<description>A comma separated list of favorite names to load into Source 2</description>
<advanced>true</advanced>
</parameter>
<parameter name="favoritesSrc3" type="text" required="false">
<label>S3 Favorites</label>
<description>A comma separated list of favorite names to load into Source 3</description>
<advanced>true</advanced>
</parameter>
<parameter name="favoritesSrc4" type="text" required="false">
<label>S4 Favorites</label>
<description>A comma separated list of favorite names to load into Source 4</description>
<advanced>true</advanced>
</parameter>
<parameter name="favoritesSrc5" type="text" required="false">
<label>S5 Favorites</label>
<description>A comma separated list of favorite names to load into Source 5</description>
<advanced>true</advanced>
</parameter>
<parameter name="favoritesSrc6" type="text" required="false">
<label>S6 Favorites</label>
<description>A comma separated list of favorite names to load into Source 6</description>
<advanced>true</advanced>
</parameter>
<parameter name="favPrefix1" type="text" required="false">
<label>S1 Favorite Prefix</label>
<description>To quickly locate Source 1's favorites, this prefix will be added to the favorite
names</description>
<advanced>true</advanced>
</parameter>
<parameter name="favPrefix2" type="text" required="false">
<label>S2 Favorite Prefix</label>
<description>To quickly locate Source 2's favorites, this prefix will be added to the favorite
names</description>
<advanced>true</advanced>
</parameter>
<parameter name="favPrefix3" type="text" required="false">
<label>S3 Favorite Prefix</label>
<description>To quickly locate Source 3's favorites, this prefix will be added to the favorite
names</description>
<advanced>true</advanced>
</parameter>
<parameter name="favPrefix4" type="text" required="false">
<label>S4 Favorite Prefix</label>
<description>To quickly locate Source 4's favorites, this prefix will be added to the favorite
names</description>
<advanced>true</advanced>
</parameter>
<parameter name="favPrefix5" type="text" required="false">
<label>S5 Favorite Prefix</label>
<description>To quickly locate Source 5's favorites, this prefix will be added to the favorite
names</description>
<advanced>true</advanced>
</parameter>
<parameter name="favPrefix6" type="text" required="false">
<label>S6 Favorite Prefix</label>
<description>To quickly locate Source 6's favorites, this prefix will be added to the favorite
names</description>
<advanced>true</advanced>
</parameter>
<parameter name="menuXmlSrc1" type="text" required="false">
<context>script</context>
<label>S1 Menu XML</label>
<description>An XML string representing the menu items to load into the keypad for Source 1, see README</description>
<advanced>true</advanced>
</parameter>
<parameter name="menuXmlSrc2" type="text" required="false">
<context>script</context>
<label>S2 Menu XML</label>
<description>An XML string representing the menu items to load into the keypad for Source 2, see README</description>
<advanced>true</advanced>
</parameter>
<parameter name="menuXmlSrc3" type="text" required="false">
<context>script</context>
<label>S3 Menu XML</label>
<description>An XML string representing the menu items to load into the keypad for Source 3, see README</description>
<advanced>true</advanced>
</parameter>
<parameter name="menuXmlSrc4" type="text" required="false">
<context>script</context>
<label>S4 Menu XML</label>
<description>An XML string representing the menu items to load into the keypad for Source 4, see README</description>
<advanced>true</advanced>
</parameter>
<parameter name="menuXmlSrc5" type="text" required="false">
<context>script</context>
<label>S5 Menu XML</label>
<description>An XML string representing the menu items to load into the keypad for Source 5, see README</description>
<advanced>true</advanced>
</parameter>
<parameter name="menuXmlSrc6" type="text" required="false">
<context>script</context>
<label>S6 Menu XML</label>
<description>An XML string representing the menu items to load into the keypad for Source 6, see README</description>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
@ -162,6 +341,7 @@
<channel id="alloff" typeId="alloff"/>
<channel id="allmute" typeId="system.mute"/>
<channel id="page" typeId="page"/>
<channel id="sendcmd" typeId="sendcmd"/>
</channels>
</channel-group-type>
@ -197,6 +377,7 @@
<channel id="track_length" typeId="track_length"/>
<channel id="track_position" typeId="track_position"/>
<channel id="button_press" typeId="button_press"/>
<channel id="art_url" typeId="art_url"/>
</channels>
</channel-group-type>
@ -212,6 +393,12 @@
<description>Activates the Page Mode for All Zones</description>
</channel-type>
<channel-type id="sendcmd">
<item-type>String</item-type>
<label>Send Command</label>
<description>Send a command to the amplifier</description>
</channel-type>
<channel-type id="source">
<item-type>Number</item-type>
<label>Source Input</label>
@ -222,22 +409,6 @@
<item-type>Number</item-type>
<label>Favorite</label>
<description>Select a Preset Favorite for the Zone</description>
<state>
<options>
<option value="1">Favorite 1</option>
<option value="2">Favorite 2</option>
<option value="3">Favorite 3</option>
<option value="4">Favorite 4</option>
<option value="5">Favorite 5</option>
<option value="6">Favorite 6</option>
<option value="7">Favorite 7</option>
<option value="8">Favorite 8</option>
<option value="9">Favorite 9</option>
<option value="10">Favorite 10</option>
<option value="11">Favorite 11</option>
<option value="12">Favorite 12</option>
</options>
</state>
</channel-type>
<channel-type id="control">
@ -350,4 +521,10 @@
<state readOnly="true"/>
</channel-type>
<channel-type id="art_url">
<item-type>String</item-type>
<label>Album Art URL</label>
<description>The URL of the Album Art JPG for this source that is displayed on a CTP-36</description>
</channel-type>
</thing:thing-descriptions>