]> git.basschouten.com Git - openhab-addons.git/blob
4eb6552711d32b498ce3c1e3e1a4e5b35655e333
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.monopriceaudio.internal.communication;
14
15 import static org.openhab.binding.monopriceaudio.internal.MonopriceAudioBindingConstants.*;
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
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.monopriceaudio.internal.MonopriceAudioException;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29
30 /**
31  * Abstract class for communicating with the MonopriceAudio device
32  *
33  * @author Laurent Garnier - Initial contribution
34  * @author Michael Lobstein - Adapted for the MonopriceAudio binding
35  * @author Michael Lobstein - Add support for additional amplifier types
36  */
37 @NonNullByDefault
38 public abstract class MonopriceAudioConnector {
39     // Message types
40     public static final String KEY_ZONE_UPDATE = "zone_update";
41     public static final String KEY_PING = "ping";
42
43     private final Logger logger = LoggerFactory.getLogger(MonopriceAudioConnector.class);
44
45     /** The output stream */
46     protected @Nullable OutputStream dataOut;
47
48     /** The input stream */
49     protected @Nullable InputStream dataIn;
50
51     /** true if the connection is established, false if not */
52     private boolean connected;
53     private boolean pingResponseOnly;
54
55     private @Nullable AmplifierModel amp;
56
57     private @Nullable Thread readerThread;
58
59     private final List<MonopriceAudioMessageEventListener> listeners = new ArrayList<>();
60
61     /**
62      * Get whether the connection is established or not
63      *
64      * @return true if the connection is established
65      */
66     public boolean isConnected() {
67         return connected;
68     }
69
70     /**
71      * Set whether the connection is established or not
72      *
73      * @param connected true if the connection is established
74      */
75     protected void setConnected(boolean connected) {
76         this.connected = connected;
77         this.pingResponseOnly = false;
78     }
79
80     /**
81      * Set the AmplifierModel
82      *
83      * @param amp the AmplifierModel being used
84      */
85     protected void setAmplifierModel(AmplifierModel amp) {
86         this.amp = amp;
87     }
88
89     /**
90      * Set the thread that handles the feedback messages
91      *
92      * @param readerThread the thread
93      */
94     protected void setReaderThread(Thread readerThread) {
95         this.readerThread = readerThread;
96     }
97
98     /**
99      * Open the connection with the MonopriceAudio device
100      *
101      * @throws MonopriceAudioException - In case of any problem
102      */
103     public abstract void open() throws MonopriceAudioException;
104
105     /**
106      * Close the connection with the MonopriceAudio device
107      */
108     public abstract void close();
109
110     /**
111      * Stop the thread that handles the feedback messages and close the opened input and output streams
112      */
113     protected void cleanup() {
114         this.pingResponseOnly = false;
115         Thread readerThread = this.readerThread;
116         OutputStream dataOut = this.dataOut;
117         if (dataOut != null) {
118             try {
119                 dataOut.close();
120             } catch (IOException e) {
121                 logger.debug("Error closing dataOut: {}", e.getMessage());
122             }
123             this.dataOut = null;
124         }
125         InputStream dataIn = this.dataIn;
126         if (dataIn != null) {
127             try {
128                 dataIn.close();
129             } catch (IOException e) {
130                 logger.debug("Error closing dataIn: {}", e.getMessage());
131             }
132             this.dataIn = null;
133         }
134         if (readerThread != null) {
135             readerThread.interrupt();
136             try {
137                 readerThread.join(3000);
138             } catch (InterruptedException e) {
139                 logger.debug("Error joining readerThread: {}", e.getMessage());
140             }
141             this.readerThread = null;
142         }
143     }
144
145     /**
146      * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
147      * actually read is returned as an integer.
148      *
149      * @param dataBuffer the buffer into which the data is read.
150      *
151      * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
152      *         stream has been reached.
153      *
154      * @throws MonopriceAudioException - If the input stream is null, if the first byte cannot be read for any reason
155      *             other than the end of the file, if the input stream has been closed, or if some other I/O error
156      *             occurs.
157      */
158     protected int readInput(byte[] dataBuffer) throws MonopriceAudioException {
159         InputStream dataIn = this.dataIn;
160         if (dataIn == null) {
161             throw new MonopriceAudioException("readInput failed: input stream is null");
162         }
163         try {
164             return dataIn.read(dataBuffer);
165         } catch (IOException e) {
166             throw new MonopriceAudioException("readInput failed: " + e.getMessage(), e);
167         }
168     }
169
170     /**
171      * Get only ping success events from the connector. If amplifier does not have keypads or supports
172      * unsolicited updates, the use of this method will cause the connector to only send ping success events until the
173      * next time the connection is reset.
174      *
175      * @throws MonopriceAudioException - In case of any problem
176      */
177     public void sendPing() throws MonopriceAudioException {
178         pingResponseOnly = true;
179         // poll zone 1 status only to see if the amp responds
180         queryZone(amp.getZoneIds().get(0));
181     }
182
183     /**
184      * Get the status of a zone
185      *
186      * @param zone the zone to query for current status
187      *
188      * @throws MonopriceAudioException - In case of any problem
189      */
190     public void queryZone(String zoneId) throws MonopriceAudioException {
191         sendCommand(amp.getQueryPrefix() + zoneId + amp.getQuerySuffix());
192     }
193
194     /**
195      * Monoprice 31028 and OSD Audio PAM1270 amps do not report treble, bass and balance with the main status inquiry,
196      * so we must send three extra commands to retrieve those values
197      *
198      * @param zone the zone to query for current treble, bass and balance status
199      *
200      * @throws MonopriceAudioException - In case of any problem
201      */
202     public void queryTrebBassBalance(String zoneId) throws MonopriceAudioException {
203         sendCommand(amp.getQueryPrefix() + zoneId + amp.getTrebleCmd());
204         sendCommand(amp.getQueryPrefix() + zoneId + amp.getBassCmd());
205         sendCommand(amp.getQueryPrefix() + zoneId + amp.getBalanceCmd());
206     }
207
208     /**
209      * Request the MonopriceAudio amplifier to execute a raw command
210      *
211      * @param cmd the command to execute
212      *
213      * @throws MonopriceAudioException - In case of any problem
214      */
215     public void sendCommand(String cmd) throws MonopriceAudioException {
216         sendCommand(null, cmd, null);
217     }
218
219     /**
220      * Request the MonopriceAudio amplifier to execute a command
221      *
222      * @param zoneId the zone for which the command is to be run
223      * @param cmd the command to execute
224      * @param value the integer value to consider for power, volume, bass, treble, etc. adjustment
225      *
226      * @throws MonopriceAudioException - In case of any problem
227      */
228     public void sendCommand(@Nullable String zoneId, String cmd, @Nullable Integer value)
229             throws MonopriceAudioException {
230         String messageStr;
231
232         if (zoneId != null && value != null) {
233             // if the command passed a value, build messageStr with prefix, zoneId, command, value and suffix
234             messageStr = amp.getCmdPrefix() + zoneId + cmd + amp.getFormattedValue(value) + amp.getCmdSuffix();
235         } else {
236             // otherwise send the raw cmd from the query() methods
237             messageStr = cmd + amp.getCmdSuffix();
238         }
239         logger.debug("Send command {}", messageStr);
240
241         OutputStream dataOut = this.dataOut;
242         if (dataOut == null) {
243             throw new MonopriceAudioException("Send command \"" + messageStr + "\" failed: output stream is null");
244         }
245         try {
246             dataOut.write(messageStr.getBytes(StandardCharsets.US_ASCII));
247             dataOut.flush();
248         } catch (IOException e) {
249             throw new MonopriceAudioException("Send command \"" + messageStr + "\" failed: " + e.getMessage(), e);
250         }
251     }
252
253     /**
254      * Add a listener to the list of listeners to be notified with events
255      *
256      * @param listener the listener
257      */
258     public void addEventListener(MonopriceAudioMessageEventListener listener) {
259         listeners.add(listener);
260     }
261
262     /**
263      * Remove a listener from the list of listeners to be notified with events
264      *
265      * @param listener the listener
266      */
267     public void removeEventListener(MonopriceAudioMessageEventListener listener) {
268         listeners.remove(listener);
269     }
270
271     /**
272      * Analyze an incoming message and dispatch corresponding (key, value) to the event listeners
273      *
274      * @param incomingMessage the received message
275      */
276     public void handleIncomingMessage(byte[] incomingMessage) {
277         if (pingResponseOnly) {
278             dispatchKeyValue(KEY_PING, EMPTY);
279             return;
280         }
281
282         String message = new String(incomingMessage, StandardCharsets.US_ASCII).trim();
283
284         if (EMPTY.equals(message)) {
285             return;
286         }
287
288         if (message.startsWith(amp.getRespPrefix())) {
289             logger.debug("handleIncomingMessage: {}", message);
290             dispatchKeyValue(KEY_ZONE_UPDATE, message);
291         } else {
292             logger.debug("no match on message: {}", message);
293         }
294     }
295
296     /**
297      * Dispatch an event (key, value) to the event listeners
298      *
299      * @param key the key
300      * @param value the value
301      */
302     private void dispatchKeyValue(String key, String value) {
303         MonopriceAudioMessageEvent event = new MonopriceAudioMessageEvent(this, key, value);
304         listeners.forEach(l -> l.onNewMessageEvent(event));
305     }
306 }