2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.nuvo.internal.communication;
15 import static org.openhab.binding.nuvo.internal.NuvoBindingConstants.*;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.OutputStream;
20 import java.nio.charset.StandardCharsets;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.nuvo.internal.NuvoException;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
33 * Abstract class for communicating with the Nuvo device
35 * @author Laurent Garnier - Initial contribution
36 * @author Michael Lobstein - Adapted for the Nuvo binding
39 public abstract class NuvoConnector {
40 private static final String COMMAND_OK = "#OK";
41 private static final String BEGIN_CMD = "*";
42 private static final String END_CMD = "\r";
43 private static final String QUERY = "?";
44 private static final String VER_STR = "#VER\"NV-";
45 private static final String ALL_OFF = "#ALLOFF";
46 private static final String MUTE = "#MUTE";
47 private static final String PAGE = "#PAGE";
49 private static final byte[] WAKE_STR = "\r".getBytes(StandardCharsets.US_ASCII);
51 private static final Pattern SRC_PATTERN = Pattern.compile("^#S(\\d{1})(.*)$");
52 private static final Pattern ZONE_PATTERN = Pattern.compile("^#Z(\\d{1,2}),(.*)$");
53 private static final Pattern ZONE_BUTTON_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})(.*)$");
54 private static final Pattern ZONE_CFG_PATTERN = Pattern.compile("^#ZCFG(\\d{1,2}),(.*)$");
56 private final Logger logger = LoggerFactory.getLogger(NuvoConnector.class);
58 protected static final String COMMAND_ERROR = "#?";
60 /** The output stream */
61 protected @Nullable OutputStream dataOut;
63 /** The input stream */
64 protected @Nullable InputStream dataIn;
66 /** true if the connection is established, false if not */
67 private boolean connected;
69 private @Nullable Thread readerThread;
71 private List<NuvoMessageEventListener> listeners = new ArrayList<>();
73 private boolean isEssentia = true;
76 * Get whether the connection is established or not
78 * @return true if the connection is established
80 public boolean isConnected() {
85 * Set whether the connection is established or not
87 * @param connected true if the connection is established
89 protected void setConnected(boolean connected) {
90 this.connected = connected;
94 * Tell the connector if the device is an Essentia G or not
96 * @param true if the device is an Essentia G
98 public void setEssentia(boolean isEssentia) {
99 this.isEssentia = isEssentia;
103 * Set the thread that handles the feedback messages
105 * @param readerThread the thread
107 protected void setReaderThread(Thread readerThread) {
108 this.readerThread = readerThread;
112 * Open the connection with the Nuvo device
114 * @throws NuvoException - In case of any problem
116 public abstract void open() throws NuvoException;
119 * Close the connection with the Nuvo device
121 public abstract void close();
124 * Stop the thread that handles the feedback messages and close the opened input and output streams
126 protected void cleanup() {
127 Thread readerThread = this.readerThread;
128 OutputStream dataOut = this.dataOut;
129 if (dataOut != null) {
132 } catch (IOException e) {
133 logger.debug("Error closing dataOut: {}", e.getMessage());
137 InputStream dataIn = this.dataIn;
138 if (dataIn != null) {
141 } catch (IOException e) {
142 logger.debug("Error closing dataIn: {}", e.getMessage());
146 if (readerThread != null) {
147 readerThread.interrupt();
148 this.readerThread = null;
150 readerThread.join(3000);
151 } catch (InterruptedException e) {
152 logger.warn("Error joining readerThread: {}", e.getMessage());
158 * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
159 * actually read is returned as an integer.
161 * @param dataBuffer the buffer into which the data is read.
163 * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
164 * stream has been reached.
166 * @throws NuvoException - If the input stream is null, if the first byte cannot be read for any reason
167 * other than the end of the file, if the input stream has been closed, or if some other I/O error
170 protected int readInput(byte[] dataBuffer) throws NuvoException {
171 InputStream dataIn = this.dataIn;
172 if (dataIn == null) {
173 throw new NuvoException("readInput failed: input stream is null");
176 return dataIn.read(dataBuffer);
177 } catch (IOException e) {
178 throw new NuvoException("readInput failed: " + e.getMessage(), e);
183 * Request the Nuvo controller to execute an inquiry command
185 * @param zone the zone for which the command is to be run
186 * @param cmd the command to execute
188 * @throws NuvoException - In case of any problem
190 public void sendQuery(NuvoEnum zone, NuvoCommand cmd) throws NuvoException {
191 sendCommand(zone.getId() + cmd.getValue() + QUERY);
195 * Request the Nuvo controller to execute a command for a zone that takes no arguments (ie power on, power off,
198 * @param zone the zone for which the command is to be run
199 * @param cmd the command to execute
201 * @throws NuvoException - In case of any problem
203 public void sendCommand(NuvoEnum zone, NuvoCommand cmd) throws NuvoException {
204 sendCommand(zone.getId() + cmd.getValue());
208 * Request the Nuvo controller to execute a command for a zone and pass in a value
210 * @param zone the zone for which the command is to be run
211 * @param cmd the command to execute
212 * @param value the string value to consider for volume, source, etc.
214 * @throws NuvoException - In case of any problem
216 public void sendCommand(NuvoEnum zone, NuvoCommand cmd, @Nullable String value) throws NuvoException {
217 sendCommand(zone.getId() + cmd.getValue() + value);
221 * Request the Nuvo controller to execute a configuration command for a zone and pass in a value
223 * @param zone the zone for which the command is to be run
224 * @param cmd the command to execute
225 * @param value the string value to consider for bass, treble, balance, etc.
227 * @throws NuvoException - In case of any problem
229 public void sendCfgCommand(NuvoEnum zone, NuvoCommand cmd, @Nullable String value) throws NuvoException {
230 sendCommand(zone.getConfigId() + cmd.getValue() + value);
234 * Request the Nuvo controller to execute a system command the does not specify a zone or value
236 * @param cmd the command to execute
238 * @throws NuvoException - In case of any problem
240 public void sendCommand(NuvoCommand cmd) throws NuvoException {
241 sendCommand(cmd.getValue());
245 * Request the Nuvo controller to execute a raw command string
247 * @param command the command string to run
249 * @throws NuvoException - In case of any problem
251 public void sendCommand(@Nullable String command) throws NuvoException {
252 String messageStr = BEGIN_CMD + command + END_CMD;
254 logger.debug("sending command: {}", messageStr);
256 OutputStream dataOut = this.dataOut;
257 if (dataOut == null) {
258 throw new NuvoException("Send command \"" + messageStr + "\" failed: output stream is null");
261 // Essentia G needs time to wake up when in standby mode
262 // I don't want to track that in the binding, so just do this always
263 if (this.isEssentia) {
264 dataOut.write(WAKE_STR);
267 dataOut.write(messageStr.getBytes(StandardCharsets.US_ASCII));
269 } catch (IOException e) {
270 throw new NuvoException("Send command \"" + command + "\" failed: " + e.getMessage(), e);
275 * Add a listener to the list of listeners to be notified with events
277 * @param listener the listener
279 public void addEventListener(NuvoMessageEventListener listener) {
280 listeners.add(listener);
284 * Remove a listener from the list of listeners to be notified with events
286 * @param listener the listener
288 public void removeEventListener(NuvoMessageEventListener listener) {
289 listeners.remove(listener);
293 * Analyze an incoming message and dispatch corresponding (type, key, value) to the event listeners
295 * @param incomingMessage the received message
297 public void handleIncomingMessage(byte[] incomingMessage) {
298 String message = new String(incomingMessage, StandardCharsets.US_ASCII).trim();
300 logger.debug("handleIncomingMessage: {}", message);
302 if (COMMAND_ERROR.equals(message) || COMMAND_OK.equals(message)) {
307 if (message.contains(VER_STR)) {
308 // example: #VER"NV-E6G FWv2.66 HWv0"
309 // split on " and return the version number
310 dispatchKeyValue(TYPE_VERSION, "", message.split("\"")[1]);
314 if (message.equals(ALL_OFF)) {
315 dispatchKeyValue(TYPE_ALLOFF, BLANK, BLANK);
319 if (message.contains(MUTE)) {
320 dispatchKeyValue(TYPE_ALLMUTE, BLANK, message.substring(message.length() - 1));
324 if (message.contains(PAGE)) {
325 dispatchKeyValue(TYPE_PAGE, BLANK, message.substring(message.length() - 1));
329 // Amp controller send a source update ie: #S2DISPINFO,DUR3380,POS3090,STATUS2
330 // or #S2DISPLINE1,"1 of 17"
331 Matcher matcher = SRC_PATTERN.matcher(message);
332 if (matcher.find()) {
333 // pull out the source id and the remainder of the message
334 dispatchKeyValue(TYPE_SOURCE_UPDATE, matcher.group(1), matcher.group(2));
338 // Amp controller send a zone update ie: #Z11,ON,SRC3,VOL63,DND0,LOCK0
339 matcher = ZONE_PATTERN.matcher(message);
340 if (matcher.find()) {
341 // pull out the zone id and the remainder of the message
342 dispatchKeyValue(TYPE_ZONE_UPDATE, matcher.group(1), matcher.group(2));
346 // Amp controller send a zone button press event ie: #Z11S3PLAYPAUSE
347 matcher = ZONE_BUTTON_PATTERN.matcher(message);
348 if (matcher.find()) {
349 // pull out the source id and the remainder of the message, ignore the zone id
350 dispatchKeyValue(TYPE_ZONE_BUTTON, matcher.group(2), matcher.group(3));
354 // Amp controller send a zone configuration response ie: #ZCFG1,BASS1,TREB-2,BALR2,LOUDCMP1
355 matcher = ZONE_CFG_PATTERN.matcher(message);
356 if (matcher.find()) {
357 // pull out the zone id and the remainder of the message
358 dispatchKeyValue(TYPE_ZONE_CONFIG, matcher.group(1), matcher.group(2));
362 logger.debug("unhandled message: {}", message);
366 * Dispatch an event (type, key, value) to the event listeners
368 * @param type the type
370 * @param value the value
372 private void dispatchKeyValue(String type, String key, String value) {
373 NuvoMessageEvent event = new NuvoMessageEvent(this, type, key, value);
374 listeners.forEach(l -> l.onNewMessageEvent(event));