]> git.basschouten.com Git - openhab-addons.git/blob
bb084ffd78240b2b4cff9a9992a20ee8f1940349
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.nuvo.internal.communication;
14
15 import static org.openhab.binding.nuvo.internal.NuvoBindingConstants.*;
16
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;
25
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;
31
32 /**
33  * Abstract class for communicating with the Nuvo device
34  *
35  * @author Laurent Garnier - Initial contribution
36  * @author Michael Lobstein - Adapted for the Nuvo binding
37  */
38 @NonNullByDefault
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_E6 = "#VER\"NV-E6G";
45     private static final String VER_STR_GC = "#VER\"NV-I8G";
46     private static final String ALL_OFF = "#ALLOFF";
47     private static final String MUTE = "#MUTE";
48     private static final String PAGE = "#PAGE";
49     private static final String RESTART = "#RESTART\"NuVoNet\"";
50     private static final String PING = "#PING";
51     private static final String PING_RESPONSE = "PING";
52
53     private static final byte[] WAKE_STR = "\r".getBytes(StandardCharsets.US_ASCII);
54
55     private static final Pattern SRC_PATTERN = Pattern.compile("^#S(\\d{1})(.*)$");
56     private static final Pattern ZONE_PATTERN = Pattern.compile("^#Z(\\d{1,2}),(.*)$");
57     private static final Pattern ZONE_BUTTON_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})(.*)$");
58     private static final Pattern ZONE_MENUREQ_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})MENUREQ(.*)$");
59     private static final Pattern ZONE_BUTTON2_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})BUTTON(.*)$");
60     private static final Pattern ZONE_BUTTONTWO_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})BUTTONTWO(.*)$");
61
62     private static final Pattern ZONE_CFG_PATTERN = Pattern.compile("^#ZCFG(\\d{1,2}),(.*)$");
63
64     // S2ALBUMARTREQ0x620FD879,80,80,2,0x00C0C0C0,0,0,0,0,1
65     private static final Pattern ALBUM_ART_REQ = Pattern.compile("^#S(\\d{1})ALBUMARTREQ(.*)$");
66
67     // S2ALBUMARTFRAGREQ0x620FD879,0,750
68     private static final Pattern ALBUM_ART_FRAG_REQ = Pattern.compile("^#S(\\d{1})ALBUMARTFRAGREQ(.*)$");
69
70     // S6FAVORITE0x000003E8
71     private static final Pattern FAVORITE_PATTERN = Pattern.compile("^#S(\\d{1})FAVORITE0x(.*)$");
72
73     private final Logger logger = LoggerFactory.getLogger(NuvoConnector.class);
74
75     protected static final String COMMAND_ERROR = "#?";
76
77     /** The output stream */
78     protected @Nullable OutputStream dataOut;
79
80     /** The input stream */
81     protected @Nullable InputStream dataIn;
82
83     /** true if the connection is established, false if not */
84     private boolean connected;
85
86     private @Nullable Thread readerThread;
87
88     private List<NuvoMessageEventListener> listeners = new ArrayList<>();
89
90     private boolean isEssentia = true;
91
92     /**
93      * Get whether the connection is established or not
94      *
95      * @return true if the connection is established
96      */
97     public boolean isConnected() {
98         return connected;
99     }
100
101     /**
102      * Set whether the connection is established or not
103      *
104      * @param connected true if the connection is established
105      */
106     protected void setConnected(boolean connected) {
107         this.connected = connected;
108     }
109
110     /**
111      * Tell the connector if the device is an Essentia G or not
112      *
113      * @param true if the device is an Essentia G
114      */
115     public void setEssentia(boolean isEssentia) {
116         this.isEssentia = isEssentia;
117     }
118
119     /**
120      * Set the thread that handles the feedback messages
121      *
122      * @param readerThread the thread
123      */
124     protected void setReaderThread(Thread readerThread) {
125         this.readerThread = readerThread;
126     }
127
128     /**
129      * Open the connection with the Nuvo device
130      *
131      * @throws NuvoException - In case of any problem
132      */
133     public abstract void open() throws NuvoException;
134
135     /**
136      * Close the connection with the Nuvo device
137      */
138     public abstract void close();
139
140     /**
141      * Stop the thread that handles the feedback messages and close the opened input and output streams
142      */
143     protected void cleanup() {
144         Thread readerThread = this.readerThread;
145         OutputStream dataOut = this.dataOut;
146         if (dataOut != null) {
147             try {
148                 dataOut.close();
149             } catch (IOException e) {
150                 logger.debug("Error closing dataOut: {}", e.getMessage());
151             }
152             this.dataOut = null;
153         }
154         InputStream dataIn = this.dataIn;
155         if (dataIn != null) {
156             try {
157                 dataIn.close();
158             } catch (IOException e) {
159                 logger.debug("Error closing dataIn: {}", e.getMessage());
160             }
161             this.dataIn = null;
162         }
163         if (readerThread != null) {
164             readerThread.interrupt();
165             this.readerThread = null;
166             try {
167                 readerThread.join(3000);
168             } catch (InterruptedException e) {
169                 logger.warn("Error joining readerThread: {}", e.getMessage());
170             }
171         }
172     }
173
174     /**
175      * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
176      * actually read is returned as an integer.
177      *
178      * @param dataBuffer the buffer into which the data is read.
179      *
180      * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
181      *         stream has been reached.
182      *
183      * @throws NuvoException - If the input stream is null, if the first byte cannot be read for any reason
184      *             other than the end of the file, if the input stream has been closed, or if some other I/O error
185      *             occurs.
186      */
187     protected int readInput(byte[] dataBuffer) throws NuvoException {
188         InputStream dataIn = this.dataIn;
189         if (dataIn == null) {
190             throw new NuvoException("readInput failed: input stream is null");
191         }
192         try {
193             return dataIn.read(dataBuffer);
194         } catch (IOException e) {
195             throw new NuvoException("readInput failed: " + e.getMessage(), e);
196         }
197     }
198
199     /**
200      * Request the Nuvo controller to execute an inquiry command
201      *
202      * @param zone the zone for which the command is to be run
203      * @param cmd the command to execute
204      *
205      * @throws NuvoException - In case of any problem
206      */
207     public void sendQuery(NuvoEnum zone, NuvoCommand cmd) throws NuvoException {
208         sendCommand(zone.getId() + cmd.getValue() + QUERY);
209     }
210
211     /**
212      * Request the Nuvo controller to execute a command for a zone that takes no arguments (ie power on, power off,
213      * etc.)
214      *
215      * @param zone the zone for which the command is to be run
216      * @param cmd the command to execute
217      *
218      * @throws NuvoException - In case of any problem
219      */
220     public void sendCommand(NuvoEnum zone, NuvoCommand cmd) throws NuvoException {
221         sendCommand(zone.getId() + cmd.getValue());
222     }
223
224     /**
225      * Request the Nuvo controller to execute a command for a zone and pass in a value
226      *
227      * @param zone the zone for which the command is to be run
228      * @param cmd the command to execute
229      * @param value the string value to consider for volume, source, etc.
230      *
231      * @throws NuvoException - In case of any problem
232      */
233     public void sendCommand(NuvoEnum zone, NuvoCommand cmd, @Nullable String value) throws NuvoException {
234         sendCommand(zone.getId() + cmd.getValue() + value);
235     }
236
237     /**
238      * Request the Nuvo controller to execute a configuration command for a zone and pass in a value
239      *
240      * @param zone the zone for which the command is to be run
241      * @param cmd the command to execute
242      * @param value the string value to consider for bass, treble, balance, etc.
243      *
244      * @throws NuvoException - In case of any problem
245      */
246     public void sendCfgCommand(NuvoEnum zone, NuvoCommand cmd, @Nullable String value) throws NuvoException {
247         sendCommand(zone.getConfigId() + cmd.getValue() + value);
248     }
249
250     /**
251      * Request the Nuvo controller to execute a system command the does not specify a zone or value
252      *
253      * @param cmd the command to execute
254      *
255      * @throws NuvoException - In case of any problem
256      */
257     public void sendCommand(NuvoCommand cmd) throws NuvoException {
258         sendCommand(cmd.getValue());
259     }
260
261     /**
262      * Request the Nuvo controller to execute a raw command string
263      *
264      * @param command the command string to run
265      *
266      * @throws NuvoException - In case of any problem
267      */
268     public void sendCommand(@Nullable String command) throws NuvoException {
269         String messageStr = BEGIN_CMD + command + END_CMD;
270
271         logger.debug("sending command: {}", messageStr);
272
273         OutputStream dataOut = this.dataOut;
274         if (dataOut == null) {
275             throw new NuvoException("Send command \"" + messageStr + "\" failed: output stream is null");
276         }
277         try {
278             // Essentia G needs time to wake up when in standby mode
279             // I don't want to track that in the binding, so just do this always
280             if (this.isEssentia) {
281                 dataOut.write(WAKE_STR);
282                 dataOut.flush();
283             }
284             dataOut.write(messageStr.getBytes(StandardCharsets.US_ASCII));
285             dataOut.flush();
286         } catch (IOException e) {
287             throw new NuvoException("Send command \"" + command + "\" failed: " + e.getMessage(), e);
288         }
289     }
290
291     /**
292      * Add a listener to the list of listeners to be notified with events
293      *
294      * @param listener the listener
295      */
296     public void addEventListener(NuvoMessageEventListener listener) {
297         listeners.add(listener);
298     }
299
300     /**
301      * Remove a listener from the list of listeners to be notified with events
302      *
303      * @param listener the listener
304      */
305     public void removeEventListener(NuvoMessageEventListener listener) {
306         listeners.remove(listener);
307     }
308
309     /**
310      * Analyze an incoming message and dispatch corresponding (type, key, value) to the event listeners
311      *
312      * @param incomingMessage the received message
313      */
314     public void handleIncomingMessage(byte[] incomingMessage) {
315         String message = new String(incomingMessage, StandardCharsets.US_ASCII).trim();
316
317         logger.debug("handleIncomingMessage: {}", message);
318
319         if (COMMAND_ERROR.equals(message) || COMMAND_OK.equals(message)) {
320             // ignore
321             return;
322         }
323
324         if (message.contains(PING)) {
325             try {
326                 sendCommand(PING_RESPONSE);
327             } catch (NuvoException e) {
328                 logger.debug("Error sending response to PING command");
329             }
330             dispatchKeyValue(TYPE_PING, BLANK, BLANK);
331             return;
332         }
333
334         if (RESTART.equals(message)) {
335             dispatchKeyValue(TYPE_RESTART, BLANK, BLANK);
336             return;
337         }
338
339         if (message.contains(VER_STR_E6) || message.contains(VER_STR_GC)) {
340             // example: #VER"NV-E6G FWv2.66 HWv0"
341             // split on " and return the version number
342             dispatchKeyValue(TYPE_VERSION, "", message.split("\"")[1]);
343             return;
344         }
345
346         if (message.equals(ALL_OFF)) {
347             dispatchKeyValue(TYPE_ALLOFF, BLANK, BLANK);
348             return;
349         }
350
351         if (message.contains(MUTE)) {
352             dispatchKeyValue(TYPE_ALLMUTE, BLANK, message.substring(message.length() - 1));
353             return;
354         }
355
356         if (message.contains(PAGE)) {
357             dispatchKeyValue(TYPE_PAGE, BLANK, message.substring(message.length() - 1));
358             return;
359         }
360
361         // Amp controller sent an album art request
362         Matcher matcher = ALBUM_ART_REQ.matcher(message);
363         if (matcher.find()) {
364             // pull out the source id and the remainder of the message
365             dispatchKeyValue(TYPE_ALBUM_ART_REQ, matcher.group(1), matcher.group(2));
366             return;
367         }
368
369         // Amp controller sent an album art fragment request
370         matcher = ALBUM_ART_FRAG_REQ.matcher(message);
371         if (matcher.find()) {
372             // pull out the source id and the remainder of the message
373             dispatchKeyValue(TYPE_ALBUM_ART_FRAG_REQ, matcher.group(1), matcher.group(2));
374             return;
375         }
376
377         // Amp controller sent a request to play a favorite
378         matcher = FAVORITE_PATTERN.matcher(message);
379         if (matcher.find()) {
380             // pull out the source id and the remainder of the message
381             dispatchKeyValue(TYPE_FAVORITE_REQ, matcher.group(1), matcher.group(2));
382             return;
383         }
384
385         // Amp controller sent a source update ie: #S2DISPINFO,DUR3380,POS3090,STATUS2
386         // or #S2DISPLINE1,"1 of 17"
387         matcher = SRC_PATTERN.matcher(message);
388         if (matcher.find()) {
389             // pull out the source id and the remainder of the message
390             dispatchKeyValue(TYPE_SOURCE_UPDATE, matcher.group(1), matcher.group(2));
391             return;
392         }
393
394         // Amp controller sent a zone update ie: #Z11,ON,SRC3,VOL63,DND0,LOCK0
395         matcher = ZONE_PATTERN.matcher(message);
396         if (matcher.find()) {
397             // pull out the zone id and the remainder of the message
398             dispatchKeyValue(TYPE_ZONE_UPDATE, matcher.group(1), matcher.group(2));
399             return;
400         }
401
402         // Amp controller sent a zone BUTTONTWO press event ie: #Z11S3BUTTONTWO4,2,0,0,0
403         matcher = ZONE_BUTTONTWO_PATTERN.matcher(message);
404         if (matcher.find()) {
405             // redundant - ignore
406             return;
407         }
408
409         // Amp controller sent a zone BUTTON press event ie: #Z4S6BUTTON1,1,0xFFFFFFFF,1,3
410         matcher = ZONE_BUTTON2_PATTERN.matcher(message);
411         if (matcher.find()) {
412             // pull out the remainder of the message: button #, action, menuid, itemid, itemidx
413             String[] buttonSplit = matcher.group(3).split(COMMA);
414
415             // second field is button action, only send DOWNUP (0) or DOWN (1), ignore UP (2)
416             if (ZERO.equals(buttonSplit[1]) || ONE.equals(buttonSplit[1])) {
417                 // a button in a menu was pressed, send SxZy,menuid,itemidx
418                 if (!ZERO.equals(buttonSplit[2])) {
419                     dispatchKeyValue(TYPE_MENU_ITEM_SELECTED, matcher.group(2), SRC_KEY + matcher.group(2) + ZONE_KEY
420                             + matcher.group(1) + COMMA + buttonSplit[2] + COMMA + buttonSplit[3]);
421                 } else {
422                     // send the button # in the event, don't send extra fields menuid, itemid, etc..
423                     dispatchKeyValue(TYPE_ZONE_BUTTON2, matcher.group(2), buttonSplit[0]);
424                 }
425             }
426             return;
427         }
428
429         // Amp controller sent a menu request event ie: #Z2S6MENUREQ0x0000000B,1,0,0
430         matcher = ZONE_MENUREQ_PATTERN.matcher(message);
431         if (matcher.find()) {
432             // pull out the source id and send SxZy plus the remainder of the message
433             dispatchKeyValue(TYPE_ZONE_MENUREQ, matcher.group(2),
434                     SRC_KEY + matcher.group(2) + ZONE_KEY + matcher.group(1) + COMMA + matcher.group(3));
435             return;
436         }
437
438         // Amp controller sent a zone button press event ie: #Z11S3PLAYPAUSE
439         matcher = ZONE_BUTTON_PATTERN.matcher(message);
440         if (matcher.find()) {
441             // pull out the source id and the remainder of the message, ignore the zone id
442             dispatchKeyValue(TYPE_ZONE_BUTTON, matcher.group(2), matcher.group(3));
443             return;
444         }
445
446         // Amp controller sent a zone configuration response ie: #ZCFG1,BASS1,TREB-2,BALR2,LOUDCMP1
447         matcher = ZONE_CFG_PATTERN.matcher(message);
448         if (matcher.find()) {
449             // pull out the zone id and the remainder of the message
450             dispatchKeyValue(TYPE_ZONE_CONFIG, matcher.group(1), matcher.group(2));
451             return;
452         }
453
454         logger.debug("unhandled message: {}", message);
455     }
456
457     /**
458      * Dispatch an event (type, key, value) to the event listeners
459      *
460      * @param type the type
461      * @param key the key
462      * @param value the value
463      */
464     private void dispatchKeyValue(String type, String key, String value) {
465         NuvoMessageEvent event = new NuvoMessageEvent(this, type, key, value);
466         listeners.forEach(l -> l.onNewMessageEvent(event));
467     }
468 }