[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
13 changed files with 1402 additions and 109 deletions

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>