[nuvo] Add zone actions for rules (#13658)

* Add zone actions for rules
* Don't scan for NuvoNet source messages if openHAB NuvoNet sources are not being used

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
mlobstein 2022-11-12 09:57:22 -06:00 committed by GitHub
parent a1270c7822
commit 160e0c2548
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 278 additions and 190 deletions

View File

@ -85,35 +85,36 @@ connection: &conNuvo
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) |
| 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) |
| sourceN#album_art (where N= 1-6) | Image | The Album Art loaded from the art_url channel for display in a UI widget (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 |
| system#buttonpress | String | Indicates the zone number followed by a comma and the last button pressed or NuvoNet menu item selected on a keypad (ReadOnly) |
| 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) |
| sourceN#album_art (where N= 1-6) | Image | The Album Art loaded from the art_url channel for display in a UI widget (ReadOnly) |
## Full Example
@ -139,6 +140,7 @@ Switch nuvo_system_alloff "All Zones Off" { channel="nuvo:amplifier:myamp:system
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" }
String nuvo_system_buttonpress "Zone Button: [%s]" { channel="nuvo:amplifier:myamp:system#buttonpress" }
// zones
Switch nuvo_z1_power "Power" { channel="nuvo:amplifier:myamp:zone1#power" }
@ -473,12 +475,57 @@ A complete XML string for the desired menu is then stored in the `menuXmlSrcN` c
<item>menu3 x</item>
<item>menu3 y</item>
</topmenu>
<topmenu text="Turn off other zones"/>
```
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.
### Rule to trigger an action based on which keypad zone where a button was pressed or menu item selected
By using the `system#buttonpress` channel it is possible to trigger an action based on which keypad zone was used to send the action.
This channel appends the zone number and a comma before the button action or menu item selection.
For example if the Play/Pause button is pressed on Zone 7, the channel will display: `7,PLAYPAUSE`
Also if a menu item from a custom menu was selected, ie: `Top menu 1` on Zone 5, the channel will display: `5,Top menu 1`
The functionality can be used to create very powerful rules as demontrated below. The following rule triggered from a menu item turns off all zones except for the zone that triggered the rule.
nuvo-turn-off-all-but-caller.rules:
```
rule "Turn off all zones except caller zone"
when
Item nuvo_system_buttonpress received update
then
var callerZone = newState.toString().split(",").get(0)
var button = newState.toString().split(",").get(1)
if (button == "Turn off other zones") {
if (callerZone != "1") {
nuvo_z1_power.sendCommand(OFF)
}
if (callerZone != "2") {
nuvo_z2_power.sendCommand(OFF)
}
if (callerZone != "3") {
nuvo_z3_power.sendCommand(OFF)
}
if (callerZone != "4") {
nuvo_z4_power.sendCommand(OFF)
}
if (callerZone != "5") {
nuvo_z5_power.sendCommand(OFF)
}
if (callerZone != "6") {
nuvo_z6_power.sendCommand(OFF)
}
}
end
```
### 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.

View File

@ -35,6 +35,7 @@ public class NuvoBindingConstants {
public static final String CHANNEL_TYPE_ALLMUTE = "allmute";
public static final String CHANNEL_TYPE_PAGE = "page";
public static final String CHANNEL_TYPE_SENDCMD = "sendcmd";
public static final String CHANNEL_TYPE_BUTTONPRESS = "buttonpress";
// zone
public static final String CHANNEL_TYPE_POWER = "power";
@ -71,14 +72,14 @@ public class NuvoBindingConstants {
public static final String TYPE_PAGE = "page";
public static final String TYPE_SOURCE_UPDATE = "source_update";
public static final String TYPE_ZONE_UPDATE = "zone_update";
public static final String TYPE_ZONE_BUTTON = "zone_button";
public static final String TYPE_ZONE_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";
public static final String TYPE_ZONE_SOURCE_BUTTON = "zone_source_button";
public static final String TYPE_NN_BUTTON = "nn_button";
public static final String TYPE_NN_MENUREQ = "nn_menureq";
public static final String TYPE_NN_MENU_ITEM_SELECTED = "nn_menu_item_selected";
public static final String TYPE_NN_ALBUM_ART_REQ = "nn_album_art_req";
public static final String TYPE_NN_ALBUM_ART_FRAG_REQ = "nn_album_art_frag_req";
public static final String TYPE_NN_FAVORITE_REQ = "nn_favorite_req";
// misc
public static final String ON = "ON";

View File

@ -54,21 +54,21 @@ public abstract class NuvoConnector {
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_SOURCE_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})(.*)$");
private static final Pattern NN_MENUREQ_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})MENUREQ(.*)$");
private static final Pattern NN_BUTTON_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})BUTTON(.*)$");
private static final Pattern NN_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(.*)$");
private static final Pattern NN_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(.*)$");
private static final Pattern NN_ALBUM_ART_FRAG_REQ = Pattern.compile("^#S(\\d{1})ALBUMARTFRAGREQ(.*)$");
// S6FAVORITE0x000003E8
private static final Pattern FAVORITE_PATTERN = Pattern.compile("^#S(\\d{1})FAVORITE0x(.*)$");
private static final Pattern NN_FAVORITE_PATTERN = Pattern.compile("^#S(\\d{1})FAVORITE0x(.*)$");
private final Logger logger = LoggerFactory.getLogger(NuvoConnector.class);
@ -88,6 +88,7 @@ public abstract class NuvoConnector {
private List<NuvoMessageEventListener> listeners = new ArrayList<>();
private boolean isEssentia = true;
private boolean isAnyOhNuvoNet = false;
/**
* Get whether the connection is established or not
@ -116,6 +117,15 @@ public abstract class NuvoConnector {
this.isEssentia = isEssentia;
}
/**
* Tell the connector to listen for NuvoNet source messages
*
* @param true if any sources are configured as openHAB NuvoNet sources
*/
public void setAnyOhNuvoNet(boolean isAnyOhNuvoNet) {
this.isAnyOhNuvoNet = isAnyOhNuvoNet;
}
/**
* Set the thread that handles the feedback messages
*
@ -307,7 +317,7 @@ public abstract class NuvoConnector {
}
/**
* Analyze an incoming message and dispatch corresponding (type, key, value) to the event listeners
* Analyze an incoming message and dispatch corresponding (type, zone, src, value) to the event listeners
*
* @param incomingMessage the received message
*/
@ -327,119 +337,117 @@ public abstract class NuvoConnector {
} catch (NuvoException e) {
logger.debug("Error sending response to PING command");
}
dispatchKeyValue(TYPE_PING, BLANK, BLANK);
dispatchKeyValue(TYPE_PING, BLANK);
return;
}
if (RESTART.equals(message)) {
dispatchKeyValue(TYPE_RESTART, BLANK, BLANK);
dispatchKeyValue(TYPE_RESTART, 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]);
dispatchKeyValue(TYPE_VERSION, message.split("\"")[1]);
return;
}
if (message.equals(ALL_OFF)) {
dispatchKeyValue(TYPE_ALLOFF, BLANK, BLANK);
dispatchKeyValue(TYPE_ALLOFF, BLANK);
return;
}
if (message.contains(MUTE)) {
dispatchKeyValue(TYPE_ALLMUTE, BLANK, message.substring(message.length() - 1));
dispatchKeyValue(TYPE_ALLMUTE, message.substring(message.length() - 1));
return;
}
if (message.contains(PAGE)) {
dispatchKeyValue(TYPE_PAGE, BLANK, message.substring(message.length() - 1));
dispatchKeyValue(TYPE_PAGE, message.substring(message.length() - 1));
return;
}
// 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;
}
Matcher matcher;
// 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;
}
if (isAnyOhNuvoNet) {
// Amp controller sent a NuvoNet album art request
matcher = NN_ALBUM_ART_REQ.matcher(message);
if (matcher.find()) {
dispatchKeyValue(TYPE_NN_ALBUM_ART_REQ, BLANK, 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 NuvoNet album art fragment request
matcher = NN_ALBUM_ART_FRAG_REQ.matcher(message);
if (matcher.find()) {
dispatchKeyValue(TYPE_NN_ALBUM_ART_FRAG_REQ, BLANK, matcher.group(1), matcher.group(2));
return;
}
// Amp controller sent a request for a NuvoNet source to play a favorite
matcher = NN_FAVORITE_PATTERN.matcher(message);
if (matcher.find()) {
dispatchKeyValue(TYPE_NN_FAVORITE_REQ, BLANK, matcher.group(1), matcher.group(2));
return;
}
}
// Amp controller sent a source update ie: #S2DISPINFO,DUR3380,POS3090,STATUS2
// or #S2DISPLINE1,"1 of 17"
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));
dispatchKeyValue(TYPE_SOURCE_UPDATE, BLANK, matcher.group(1), matcher.group(2));
return;
}
// 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
dispatchKeyValue(TYPE_ZONE_UPDATE, matcher.group(1), matcher.group(2));
dispatchKeyValue(TYPE_ZONE_UPDATE, matcher.group(1), BLANK, matcher.group(2));
return;
}
// 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]);
}
if (isAnyOhNuvoNet) {
// Amp controller sent a NuvoNet zone/source BUTTONTWO press event ie: #Z11S3BUTTONTWO4,2,0,0,0
matcher = NN_BUTTONTWO_PATTERN.matcher(message);
if (matcher.find()) {
// redundant - ignore
return;
}
// Amp controller sent a NuvoNet zone/source BUTTON press event ie: #Z4S6BUTTON1,1,0xFFFFFFFF,1,3
matcher = NN_BUTTON_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 'menuid,itemidx'
if (!ZERO.equals(buttonSplit[2])) {
dispatchKeyValue(TYPE_NN_MENU_ITEM_SELECTED, matcher.group(1), matcher.group(2),
buttonSplit[2] + COMMA + buttonSplit[3]);
} else {
// send the button # in the event, don't send extra fields menuid, itemid, etc..
dispatchKeyValue(TYPE_NN_BUTTON, matcher.group(1), matcher.group(2), buttonSplit[0]);
}
}
return;
}
// Amp controller sent a NuvoNet zone/source menu request event ie: #Z2S6MENUREQ0x0000000B,1,0,0
matcher = NN_MENUREQ_PATTERN.matcher(message);
if (matcher.find()) {
dispatchKeyValue(TYPE_NN_MENUREQ, matcher.group(1), matcher.group(2), matcher.group(3));
return;
}
return;
}
// Amp controller sent a menu request event ie: #Z2S6MENUREQ0x0000000B,1,0,0
matcher = ZONE_MENUREQ_PATTERN.matcher(message);
// Amp controller sent a zone/source button press event ie: #Z11S3PLAYPAUSE
matcher = ZONE_SOURCE_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
dispatchKeyValue(TYPE_ZONE_BUTTON, matcher.group(2), matcher.group(3));
dispatchKeyValue(TYPE_ZONE_SOURCE_BUTTON, matcher.group(1), matcher.group(2), matcher.group(3));
return;
}
@ -447,7 +455,7 @@ public abstract class NuvoConnector {
matcher = ZONE_CFG_PATTERN.matcher(message);
if (matcher.find()) {
// pull out the zone id and the remainder of the message
dispatchKeyValue(TYPE_ZONE_CONFIG, matcher.group(1), matcher.group(2));
dispatchKeyValue(TYPE_ZONE_CONFIG, matcher.group(1), BLANK, matcher.group(2));
return;
}
@ -455,14 +463,25 @@ public abstract class NuvoConnector {
}
/**
* Dispatch an event (type, key, value) to the event listeners
* Dispatch a system level event without zone or src to the event listeners
*
* @param type the type
* @param key the key
* @param value the value
*/
private void dispatchKeyValue(String type, String key, String value) {
NuvoMessageEvent event = new NuvoMessageEvent(this, type, key, value);
private void dispatchKeyValue(String type, String value) {
dispatchKeyValue(type, BLANK, BLANK, value);
}
/**
* Dispatch an event (type, zone, src, value) to the event listeners
*
* @param type the type
* @param zone the zone id
* @param src the source id
* @param value the value
*/
private void dispatchKeyValue(String type, String zone, String src, String value) {
NuvoMessageEvent event = new NuvoMessageEvent(this, type, zone, src, value);
listeners.forEach(l -> l.onNewMessageEvent(event));
}
}

View File

@ -25,13 +25,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
public class NuvoMessageEvent extends EventObject {
private static final long serialVersionUID = 1L;
private final String type;
private final String key;
private final String zone;
private final String src;
private final String value;
public NuvoMessageEvent(Object source, String type, String key, String value) {
public NuvoMessageEvent(Object source, String type, String zone, String src, String value) {
super(source);
this.type = type;
this.key = key;
this.zone = zone;
this.src = src;
this.value = value;
}
@ -39,8 +41,12 @@ public class NuvoMessageEvent extends EventObject {
return type;
}
public String getKey() {
return key;
public String getZone() {
return zone;
}
public String getSrc() {
return src;
}
public String getValue() {

View File

@ -215,7 +215,7 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
if (serialPort != null && !serialPort.isEmpty()) {
connector = new NuvoSerialConnector(serialPortManager, serialPort, uid);
} else if (port != null) {
} else if (host != null && port != null) {
connector = new NuvoIpConnector(host, port, uid);
this.isMps4 = (port.intValue() == MPS4_PORT);
} else {
@ -239,6 +239,7 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
if (this.isAnyOhNuvoNet) {
logger.debug("At least one source is configured as an openHAB NuvoNet source");
connector.setAnyOhNuvoNet(true);
loadMenuConfiguration(config);
favoriteMap.put("1",
@ -640,11 +641,12 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
*/
@Override
public void onNewMessageEvent(NuvoMessageEvent evt) {
logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
logger.debug("onNewMessageEvent: zone {}, source {}, value {}", evt.getZone(), evt.getSrc(), evt.getValue());
lastEventReceived = System.currentTimeMillis();
String type = evt.getType();
String key = evt.getKey();
String zoneId = evt.getZone();
String srcId = evt.getSrc();
String updateData = evt.getValue().trim();
if (this.getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.versionString);
@ -687,8 +689,8 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_PAGE, ONE.equals(updateData) ? ON : OFF);
break;
case TYPE_SOURCE_UPDATE:
logger.debug("Source update: Source: {} - Value: {}", key, updateData);
NuvoEnum targetSource = NuvoEnum.valueOf(SOURCE + key);
logger.debug("Source update: Source: {} - Value: {}", srcId, updateData);
NuvoEnum targetSource = NuvoEnum.valueOf(SOURCE + srcId);
if (updateData.contains(DISPLINE)) {
// example: DISPLINE2,"Play My Song (Featuring Dee Ajayi)"
@ -712,16 +714,16 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
} else if (updateData.contains(NAME_QUOTE)) {
// example: NAME"Ipod"
String name = updateData.split("\"")[1];
sourceLabels.put(key, name);
sourceLabels.put(srcId, name);
}
break;
case TYPE_ZONE_UPDATE:
logger.debug("Zone update: Zone: {} - Value: {}", key, updateData);
logger.debug("Zone update: Zone: {} - Value: {}", zoneId, updateData);
// example : OFF
// or: ON,SRC3,VOL63,DND0,LOCK0
// or: ON,SRC3,MUTE,DND0,LOCK0
NuvoEnum targetZone = NuvoEnum.valueOf(ZONE + key);
NuvoEnum targetZone = NuvoEnum.valueOf(ZONE + zoneId);
if (OFF.equals(updateData)) {
updateChannelState(targetZone, CHANNEL_TYPE_POWER, OFF);
@ -746,35 +748,43 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
}
}
break;
case TYPE_ZONE_BUTTON:
logger.debug("Zone Button pressed: Source: {} - Button: {}", key, updateData);
updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, updateData);
case TYPE_ZONE_SOURCE_BUTTON:
logger.debug("Source Button pressed: Source: {} - Button: {}", srcId, updateData);
updateChannelState(NuvoEnum.valueOf(SOURCE + srcId), CHANNEL_BUTTON_PRESS, updateData);
updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_BUTTONPRESS, zoneId + COMMA + updateData);
break;
case TYPE_ZONE_BUTTON2:
case TYPE_NN_BUTTON:
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);
logger.debug("NuvoNet Source Button pressed: Source: {} - Button: {}", srcId, buttonAction);
updateChannelState(NuvoEnum.valueOf(SOURCE + srcId), CHANNEL_BUTTON_PRESS, buttonAction);
updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_BUTTONPRESS, zoneId + COMMA + buttonAction);
} else {
logger.debug("Zone NuvoNet Button pressed: Source: {} - Unknown button code: {}", key, updateData);
updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, updateData);
logger.debug("NuvoNet Source Button pressed: Source: {} - Unknown button code: {}", srcId,
updateData);
updateChannelState(NuvoEnum.valueOf(SOURCE + srcId), CHANNEL_BUTTON_PRESS, updateData);
updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_BUTTONPRESS, zoneId + COMMA + updateData);
}
break;
case TYPE_MENU_ITEM_SELECTED:
case TYPE_NN_MENU_ITEM_SELECTED:
// ignore this update unless openHAB is handling this source
if (nuvoNetSrcMap.get(key).equals(2)) {
if (nuvoNetSrcMap.get(srcId).equals(2)) {
String sourceZone = SRC_KEY + srcId + ZONE_KEY + zoneId;
String[] updateDataSplit = updateData.split(COMMA);
String zoneSource = updateDataSplit[0];
String menuId = updateDataSplit[1];
int menuItemIdx = Integer.parseInt(updateDataSplit[2]) - 1;
String menuId = updateDataSplit[0];
int menuItemIdx = Integer.parseInt(updateDataSplit[1]) - 1;
boolean exitMenu = false;
if ("0xFFFFFFFF".equals(menuId)) {
TopMenu topMenuItem = nuvoMenus.getSource().get(Integer.parseInt(key) - 1).getTopMenu()
TopMenu topMenuItem = nuvoMenus.getSource().get(Integer.parseInt(srcId) - 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());
logger.debug("Top Menu item selected: Source: {} - Menu Item: {}", srcId,
topMenuItem.getText());
updateChannelState(NuvoEnum.valueOf(SOURCE + srcId), CHANNEL_BUTTON_PRESS,
topMenuItem.getText());
updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_BUTTONPRESS,
zoneId + COMMA + topMenuItem.getText());
List<String> subMenuItems = topMenuItem.getItems();
@ -784,135 +794,139 @@ public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventLis
// send submenu (maximum of 20 items)
int subMenuSize = subMenuItems.size() < 20 ? subMenuItems.size() : 20;
try {
connector.sendCommand(zoneSource + "MENU" + (menuItemIdx + 11) + ",0,0," + subMenuSize
connector.sendCommand(sourceZone + "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) + "\"");
sourceZone + "MENUITEM" + (i + 1) + ",0,0,\"" + subMenuItems.get(i) + "\"");
}
} catch (NuvoException | InterruptedException e) {
logger.debug("Error sending sub menu for {}", zoneSource);
logger.debug("Error sending sub menu to {}", sourceZone);
}
}
} else {
// a sub menu item was selected
TopMenu topMenuItem = nuvoMenus.getSource().get(Integer.parseInt(key) - 1).getTopMenu()
TopMenu topMenuItem = nuvoMenus.getSource().get(Integer.parseInt(srcId) - 1).getTopMenu()
.get(Integer.decode(menuId) - 11);
String subMenuItem = topMenuItem.getItems().get(menuItemIdx);
logger.debug("Sub Menu item selected: Source: {} - Menu Item: {}", key,
logger.debug("Sub Menu item selected: Source: {} - Menu Item: {}", srcId,
topMenuItem.getText() + "|" + subMenuItem);
updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS,
updateChannelState(NuvoEnum.valueOf(SOURCE + srcId), CHANNEL_BUTTON_PRESS,
topMenuItem.getText() + "|" + subMenuItem);
updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_BUTTONPRESS,
zoneId + COMMA + 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,\"\"");
connector.sendCommand(sourceZone + "MENU0,0,0,0,0,0,0,\"\"");
} catch (NuvoException e) {
logger.debug("Error sending exit menu command for {}", zoneSource);
logger.debug("Error sending exit menu command to {}", sourceZone);
}
}
}
break;
case TYPE_ZONE_MENUREQ:
case TYPE_NN_MENUREQ:
// ignore this update unless openHAB is handling this source
if (nuvoNetSrcMap.get(key).equals(2)) {
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
if (nuvoNetSrcMap.get(srcId).equals(2)) {
logger.debug("Menu Request: Source: {} - Value: {}", srcId, updateData);
String sourceZone = SRC_KEY + srcId + ZONE_KEY + zoneId;
// For now we only support one level deep menus. If second field is '1', indicates go back to main
// menu.
String[] menuDataSplit = updateData.split(",");
if (menuDataSplit.length > 3 && ONE.equals(menuDataSplit[2])) {
String[] menuDataSplit = updateData.split(COMMA);
if (menuDataSplit.length > 2 && ONE.equals(menuDataSplit[1])) {
try {
connector.sendCommand(menuDataSplit[0] + "MENU0xFFFFFFFF,0,0,0,0,0,0,\"\"");
connector.sendCommand(sourceZone + "MENU0xFFFFFFFF,0,0,0,0,0,0,\"\"");
} catch (NuvoException e) {
logger.debug("Error sending main menu command for {}", menuDataSplit[0]);
logger.debug("Error sending main menu command to {}", sourceZone);
}
}
}
break;
case TYPE_ZONE_CONFIG:
logger.debug("Zone Configuration: Zone: {} - Value: {}", key, updateData);
logger.debug("Zone Configuration: Zone: {} - Value: {}", zoneId, updateData);
// example: BASS1,TREB-2,BALR2,LOUDCMP1
Matcher matcher = ZONE_CFG_PATTERN.matcher(updateData);
if (matcher.find()) {
updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_BASS, matcher.group(1));
updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_TREBLE, matcher.group(2));
updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_BALANCE,
updateChannelState(NuvoEnum.valueOf(ZONE + zoneId), CHANNEL_TYPE_BASS, matcher.group(1));
updateChannelState(NuvoEnum.valueOf(ZONE + zoneId), CHANNEL_TYPE_TREBLE, matcher.group(2));
updateChannelState(NuvoEnum.valueOf(ZONE + zoneId), CHANNEL_TYPE_BALANCE,
NuvoStatusCodes.getBalanceFromStr(matcher.group(3)));
updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_LOUDNESS,
updateChannelState(NuvoEnum.valueOf(ZONE + zoneId), CHANNEL_TYPE_LOUDNESS,
ONE.equals(matcher.group(4)) ? ON : OFF);
} else {
logger.debug("no match on message: {}", updateData);
}
break;
case TYPE_ALBUM_ART_REQ:
case TYPE_NN_ALBUM_ART_REQ:
// ignore this update unless openHAB is handling this source
if (nuvoNetSrcMap.get(key).equals(2)) {
logger.debug("Album Art Request for Source: {} - Data: {}", key, updateData);
if (nuvoNetSrcMap.get(srcId).equals(2)) {
logger.debug("Album Art Request for Source: {} - Data: {}", srcId, 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]));
albumArtIds.put(SRC_KEY + srcId, 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);
if (albumArtMap.get(SRC_KEY + srcId).length > 1) {
connector.sendCommand(
SRC_KEY + srcId + ALBUM_ART_AVAILABLE + albumArtIds.get(SRC_KEY + srcId) + COMMA
+ albumArtMap.get(SRC_KEY + srcId).length);
} else {
connector.sendCommand(SRC_KEY + key + ALBUM_ART_AVAILABLE + ZERO_COMMA);
connector.sendCommand(SRC_KEY + srcId + ALBUM_ART_AVAILABLE + ZERO_COMMA);
}
} catch (NuvoException e) {
logger.debug("Error sending ALBUMARTAVAILABLE command for source: {}", key);
logger.debug("Error sending ALBUMARTAVAILABLE command for source: {}", srcId);
}
}
break;
case TYPE_ALBUM_ART_FRAG_REQ:
case TYPE_NN_ALBUM_ART_FRAG_REQ:
// ignore this update unless openHAB is handling this source
if (nuvoNetSrcMap.get(key).equals(2)) {
logger.debug("Album Art Fragment Request for Source: {} - Data: {}", key, updateData);
if (nuvoNetSrcMap.get(srcId).equals(2)) {
logger.debug("Album Art Fragment Request for Source: {} - Data: {}", srcId, 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)) {
if (requestedId == albumArtIds.get(SRC_KEY + srcId)) {
byte[] chunk = new byte[length];
byte[] albumArtBytes = albumArtMap.get(SRC_KEY + key);
byte[] albumArtBytes = albumArtMap.get(SRC_KEY + srcId);
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
connector.sendCommand(SRC_KEY + srcId + ALBUM_ART_FRAG + requestedId + COMMA + offset
+ COMMA + frag.length() + COMMA + frag);
} catch (NuvoException e) {
logger.debug("Error sending ALBUMARTFRAG command for source: {}, artId: {}", key,
logger.debug("Error sending ALBUMARTFRAG command for source: {}, artId: {}", srcId,
requestedId);
}
}
}
}
break;
case TYPE_FAVORITE_REQ:
case TYPE_NN_FAVORITE_REQ:
// ignore this update unless openHAB is handling this source
if (nuvoNetSrcMap.get(key).equals(2)) {
logger.debug("Favorite request for source: {} - favoriteId: {}", key, updateData);
if (nuvoNetSrcMap.get(srcId).equals(2)) {
logger.debug("Favorite request for source: {} - favoriteId: {}", srcId, updateData);
try {
int playlistIdx = Integer.parseInt(updateData, 16) - 1000;
updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS,
"PLAY_MUSIC_PRESET:" + favoriteMap.get(key)[playlistIdx]);
updateChannelState(NuvoEnum.valueOf(SOURCE + srcId), CHANNEL_BUTTON_PRESS,
"PLAY_MUSIC_PRESET:" + favoriteMap.get(srcId)[playlistIdx]);
} catch (NumberFormatException nfe) {
logger.debug("Unable to parse favoriteId: {}", updateData);
}
}
break;
default:
logger.debug("onNewMessageEvent: unhandled key {}", key);
logger.debug("onNewMessageEvent: unhandled event type {}", type);
// Return here because receiving an unknown message does not indicate that one can poll
return;
}

View File

@ -165,7 +165,7 @@ channel-type.nuvo.balance.description = Adjust the Balance Setting for the Zone
channel-type.nuvo.bass.label = Bass Adjustment
channel-type.nuvo.bass.description = Adjust the Bass Setting for the Zone
channel-type.nuvo.button_press.label = Button Pressed
channel-type.nuvo.button_press.description = Indicates the Last Button Pressed On the Keypad for a Non NuvoNet Source
channel-type.nuvo.button_press.description = Indicates the Last Button Pressed On the Keypad
channel-type.nuvo.control.label = Control
channel-type.nuvo.control.description = Transport Controls e.g. Play/Pause/Next/Previous for the Current Source
channel-type.nuvo.display_line1.label = Display Line 1

View File

@ -342,6 +342,7 @@
<channel id="allmute" typeId="system.mute"/>
<channel id="page" typeId="page"/>
<channel id="sendcmd" typeId="sendcmd"/>
<channel id="buttonpress" typeId="button_press"/>
</channels>
</channel-group-type>
@ -518,7 +519,7 @@
<channel-type id="button_press">
<item-type>String</item-type>
<label>Button Pressed</label>
<description>Indicates the Last Button Pressed On the Keypad for a Non NuvoNet Source</description>
<description>Indicates the Last Button Pressed On the Keypad</description>
<state readOnly="true"/>
</channel-type>