]> git.basschouten.com Git - openhab-addons.git/blob
e47db3229f34cbe62670815421d9a01baeed2f26
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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 = "#VER\"NV-";
45     private static final String ALL_OFF = "#ALLOFF";
46     private static final String MUTE = "#MUTE";
47     private static final String PAGE = "#PAGE";
48
49     private static final byte[] WAKE_STR = "\r".getBytes(StandardCharsets.US_ASCII);
50
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}),(.*)$");
55
56     private final Logger logger = LoggerFactory.getLogger(NuvoConnector.class);
57
58     protected static final String COMMAND_ERROR = "#?";
59
60     /** The output stream */
61     protected @Nullable OutputStream dataOut;
62
63     /** The input stream */
64     protected @Nullable InputStream dataIn;
65
66     /** true if the connection is established, false if not */
67     private boolean connected;
68
69     private @Nullable Thread readerThread;
70
71     private List<NuvoMessageEventListener> listeners = new ArrayList<>();
72
73     private boolean isEssentia = true;
74
75     /**
76      * Get whether the connection is established or not
77      *
78      * @return true if the connection is established
79      */
80     public boolean isConnected() {
81         return connected;
82     }
83
84     /**
85      * Set whether the connection is established or not
86      *
87      * @param connected true if the connection is established
88      */
89     protected void setConnected(boolean connected) {
90         this.connected = connected;
91     }
92
93     /**
94      * Tell the connector if the device is an Essentia G or not
95      *
96      * @param true if the device is an Essentia G
97      */
98     public void setEssentia(boolean isEssentia) {
99         this.isEssentia = isEssentia;
100     }
101
102     /**
103      * Set the thread that handles the feedback messages
104      *
105      * @param readerThread the thread
106      */
107     protected void setReaderThread(Thread readerThread) {
108         this.readerThread = readerThread;
109     }
110
111     /**
112      * Open the connection with the Nuvo device
113      *
114      * @throws NuvoException - In case of any problem
115      */
116     public abstract void open() throws NuvoException;
117
118     /**
119      * Close the connection with the Nuvo device
120      */
121     public abstract void close();
122
123     /**
124      * Stop the thread that handles the feedback messages and close the opened input and output streams
125      */
126     protected void cleanup() {
127         Thread readerThread = this.readerThread;
128         OutputStream dataOut = this.dataOut;
129         if (dataOut != null) {
130             try {
131                 dataOut.close();
132             } catch (IOException e) {
133                 logger.debug("Error closing dataOut: {}", e.getMessage());
134             }
135             this.dataOut = null;
136         }
137         InputStream dataIn = this.dataIn;
138         if (dataIn != null) {
139             try {
140                 dataIn.close();
141             } catch (IOException e) {
142                 logger.debug("Error closing dataIn: {}", e.getMessage());
143             }
144             this.dataIn = null;
145         }
146         if (readerThread != null) {
147             readerThread.interrupt();
148             this.readerThread = null;
149             try {
150                 readerThread.join(3000);
151             } catch (InterruptedException e) {
152                 logger.warn("Error joining readerThread: {}", e.getMessage());
153             }
154         }
155     }
156
157     /**
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.
160      *
161      * @param dataBuffer the buffer into which the data is read.
162      *
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.
165      *
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
168      *             occurs.
169      */
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");
174         }
175         try {
176             return dataIn.read(dataBuffer);
177         } catch (IOException e) {
178             throw new NuvoException("readInput failed: " + e.getMessage(), e);
179         }
180     }
181
182     /**
183      * Request the Nuvo controller to execute an inquiry command
184      *
185      * @param zone the zone for which the command is to be run
186      * @param cmd the command to execute
187      *
188      * @throws NuvoException - In case of any problem
189      */
190     public void sendQuery(NuvoEnum zone, NuvoCommand cmd) throws NuvoException {
191         sendCommand(zone.getId() + cmd.getValue() + QUERY);
192     }
193
194     /**
195      * Request the Nuvo controller to execute a command for a zone that takes no arguments (ie power on, power off,
196      * etc.)
197      *
198      * @param zone the zone for which the command is to be run
199      * @param cmd the command to execute
200      *
201      * @throws NuvoException - In case of any problem
202      */
203     public void sendCommand(NuvoEnum zone, NuvoCommand cmd) throws NuvoException {
204         sendCommand(zone.getId() + cmd.getValue());
205     }
206
207     /**
208      * Request the Nuvo controller to execute a command for a zone and pass in a value
209      *
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.
213      *
214      * @throws NuvoException - In case of any problem
215      */
216     public void sendCommand(NuvoEnum zone, NuvoCommand cmd, @Nullable String value) throws NuvoException {
217         sendCommand(zone.getId() + cmd.getValue() + value);
218     }
219
220     /**
221      * Request the Nuvo controller to execute a configuration command for a zone and pass in a value
222      *
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.
226      *
227      * @throws NuvoException - In case of any problem
228      */
229     public void sendCfgCommand(NuvoEnum zone, NuvoCommand cmd, @Nullable String value) throws NuvoException {
230         sendCommand(zone.getConfigId() + cmd.getValue() + value);
231     }
232
233     /**
234      * Request the Nuvo controller to execute a system command the does not specify a zone or value
235      *
236      * @param cmd the command to execute
237      *
238      * @throws NuvoException - In case of any problem
239      */
240     public void sendCommand(NuvoCommand cmd) throws NuvoException {
241         sendCommand(cmd.getValue());
242     }
243
244     /**
245      * Request the Nuvo controller to execute a raw command string
246      *
247      * @param command the command string to run
248      *
249      * @throws NuvoException - In case of any problem
250      */
251     public void sendCommand(@Nullable String command) throws NuvoException {
252         String messageStr = BEGIN_CMD + command + END_CMD;
253
254         logger.debug("sending command: {}", messageStr);
255
256         OutputStream dataOut = this.dataOut;
257         if (dataOut == null) {
258             throw new NuvoException("Send command \"" + messageStr + "\" failed: output stream is null");
259         }
260         try {
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);
265                 dataOut.flush();
266             }
267             dataOut.write(messageStr.getBytes(StandardCharsets.US_ASCII));
268             dataOut.flush();
269         } catch (IOException e) {
270             throw new NuvoException("Send command \"" + command + "\" failed: " + e.getMessage(), e);
271         }
272     }
273
274     /**
275      * Add a listener to the list of listeners to be notified with events
276      *
277      * @param listener the listener
278      */
279     public void addEventListener(NuvoMessageEventListener listener) {
280         listeners.add(listener);
281     }
282
283     /**
284      * Remove a listener from the list of listeners to be notified with events
285      *
286      * @param listener the listener
287      */
288     public void removeEventListener(NuvoMessageEventListener listener) {
289         listeners.remove(listener);
290     }
291
292     /**
293      * Analyze an incoming message and dispatch corresponding (type, key, value) to the event listeners
294      *
295      * @param incomingMessage the received message
296      */
297     public void handleIncomingMessage(byte[] incomingMessage) {
298         String message = new String(incomingMessage, StandardCharsets.US_ASCII).trim();
299
300         logger.debug("handleIncomingMessage: {}", message);
301
302         if (COMMAND_ERROR.equals(message) || COMMAND_OK.equals(message)) {
303             // ignore
304             return;
305         }
306
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]);
311             return;
312         }
313
314         if (message.equals(ALL_OFF)) {
315             dispatchKeyValue(TYPE_ALLOFF, BLANK, BLANK);
316             return;
317         }
318
319         if (message.contains(MUTE)) {
320             dispatchKeyValue(TYPE_ALLMUTE, BLANK, message.substring(message.length() - 1));
321             return;
322         }
323
324         if (message.contains(PAGE)) {
325             dispatchKeyValue(TYPE_PAGE, BLANK, message.substring(message.length() - 1));
326             return;
327         }
328
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));
335             return;
336         }
337
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));
343             return;
344         }
345
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));
351             return;
352         }
353
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));
359             return;
360         }
361
362         logger.debug("unhandled message: {}", message);
363     }
364
365     /**
366      * Dispatch an event (type, key, value) to the event listeners
367      *
368      * @param type the type
369      * @param key the key
370      * @param value the value
371      */
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));
375     }
376 }