]> git.basschouten.com Git - openhab-addons.git/blob
1ad00ef909a71f3b05b3302dbcc57378838b697b
[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 java.io.IOException;
16 import java.io.InputStream;
17 import java.io.OutputStream;
18 import java.nio.charset.StandardCharsets;
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.regex.Matcher;
22 import java.util.regex.Pattern;
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  */
36 @NonNullByDefault
37 public abstract class MonopriceAudioConnector {
38     public static final String READ_ERROR = "Command Error.";
39
40     // Message types
41     public static final String KEY_ZONE_UPDATE = "zone_update";
42     // Special keys used by the binding
43     public static final String KEY_ERROR = "error";
44     public static final String MSG_VALUE_ON = "on";
45
46     private static final Pattern PATTERN = Pattern.compile("^.*#>(\\d{22})$", Pattern.DOTALL);
47     private static final String BEGIN_CMD = "<";
48     private static final String END_CMD = "\r";
49
50     private final Logger logger = LoggerFactory.getLogger(MonopriceAudioConnector.class);
51
52     /** The output stream */
53     protected @Nullable OutputStream dataOut;
54
55     /** The input stream */
56     protected @Nullable InputStream dataIn;
57
58     /** true if the connection is established, false if not */
59     private boolean connected;
60
61     private @Nullable Thread readerThread;
62
63     private final List<MonopriceAudioMessageEventListener> listeners = new ArrayList<>();
64
65     /**
66      * Get whether the connection is established or not
67      *
68      * @return true if the connection is established
69      */
70     public boolean isConnected() {
71         return connected;
72     }
73
74     /**
75      * Set whether the connection is established or not
76      *
77      * @param connected true if the connection is established
78      */
79     protected void setConnected(boolean connected) {
80         this.connected = connected;
81     }
82
83     /**
84      * Set the thread that handles the feedback messages
85      *
86      * @param readerThread the thread
87      */
88     protected void setReaderThread(Thread readerThread) {
89         this.readerThread = readerThread;
90     }
91
92     /**
93      * Open the connection with the MonopriceAudio device
94      *
95      * @throws MonopriceAudioException - In case of any problem
96      */
97     public abstract void open() throws MonopriceAudioException;
98
99     /**
100      * Close the connection with the MonopriceAudio device
101      */
102     public abstract void close();
103
104     /**
105      * Stop the thread that handles the feedback messages and close the opened input and output streams
106      */
107     protected void cleanup() {
108         Thread readerThread = this.readerThread;
109         OutputStream dataOut = this.dataOut;
110         if (dataOut != null) {
111             try {
112                 dataOut.close();
113             } catch (IOException e) {
114                 logger.debug("Error closing dataOut: {}", e.getMessage());
115             }
116             this.dataOut = null;
117         }
118         InputStream dataIn = this.dataIn;
119         if (dataIn != null) {
120             try {
121                 dataIn.close();
122             } catch (IOException e) {
123                 logger.debug("Error closing dataIn: {}", e.getMessage());
124             }
125             this.dataIn = null;
126         }
127         if (readerThread != null) {
128             readerThread.interrupt();
129             try {
130                 readerThread.join(3000);
131             } catch (InterruptedException e) {
132                 logger.warn("Error joining readerThread: {}", e.getMessage());
133             }
134             this.readerThread = null;
135         }
136     }
137
138     /**
139      * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
140      * actually read is returned as an integer.
141      *
142      * @param dataBuffer the buffer into which the data is read.
143      *
144      * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
145      *         stream has been reached.
146      *
147      * @throws MonopriceAudioException - If the input stream is null, if the first byte cannot be read for any reason
148      *             other than the end of the file, if the input stream has been closed, or if some other I/O error
149      *             occurs.
150      */
151     protected int readInput(byte[] dataBuffer) throws MonopriceAudioException {
152         InputStream dataIn = this.dataIn;
153         if (dataIn == null) {
154             throw new MonopriceAudioException("readInput failed: input stream is null");
155         }
156         try {
157             return dataIn.read(dataBuffer);
158         } catch (IOException e) {
159             throw new MonopriceAudioException("readInput failed: " + e.getMessage(), e);
160         }
161     }
162
163     /**
164      * Get the status of a zone
165      *
166      * @param zone the zone to query for current status
167      *
168      * @throws MonopriceAudioException - In case of any problem
169      */
170     public void queryZone(MonopriceAudioZone zone) throws MonopriceAudioException {
171         sendCommand(zone, MonopriceAudioCommand.QUERY, null);
172     }
173
174     /**
175      * Request the MonopriceAudio controller to execute a command
176      *
177      * @param zone the zone for which the command is to be run
178      * @param cmd the command to execute
179      * @param value the integer value to consider for volume, bass, treble, etc. adjustment
180      *
181      * @throws MonopriceAudioException - In case of any problem
182      */
183     public void sendCommand(MonopriceAudioZone zone, MonopriceAudioCommand cmd, @Nullable Integer value)
184             throws MonopriceAudioException {
185         String messageStr = "";
186
187         if (cmd == MonopriceAudioCommand.QUERY) {
188             // query special case (ie: ? + zoneId)
189             messageStr = cmd.getValue() + zone.getZoneId();
190         } else if (value != null) {
191             // if the command passed a value, append it to the messageStr
192             messageStr = BEGIN_CMD + zone.getZoneId() + cmd.getValue() + String.format("%02d", value);
193         } else {
194             throw new MonopriceAudioException("Send command \"" + messageStr + "\" failed: passed in value is null");
195         }
196         messageStr += END_CMD;
197         logger.debug("Send command {}", messageStr);
198
199         OutputStream dataOut = this.dataOut;
200         if (dataOut == null) {
201             throw new MonopriceAudioException("Send command \"" + messageStr + "\" failed: output stream is null");
202         }
203         try {
204             dataOut.write(messageStr.getBytes(StandardCharsets.US_ASCII));
205             dataOut.flush();
206         } catch (IOException e) {
207             throw new MonopriceAudioException("Send command \"" + cmd.getValue() + "\" failed: " + e.getMessage(), e);
208         }
209     }
210
211     /**
212      * Add a listener to the list of listeners to be notified with events
213      *
214      * @param listener the listener
215      */
216     public void addEventListener(MonopriceAudioMessageEventListener listener) {
217         listeners.add(listener);
218     }
219
220     /**
221      * Remove a listener from the list of listeners to be notified with events
222      *
223      * @param listener the listener
224      */
225     public void removeEventListener(MonopriceAudioMessageEventListener listener) {
226         listeners.remove(listener);
227     }
228
229     /**
230      * Analyze an incoming message and dispatch corresponding (key, value) to the event listeners
231      *
232      * @param incomingMessage the received message
233      */
234     public void handleIncomingMessage(byte[] incomingMessage) {
235         String message = new String(incomingMessage, StandardCharsets.US_ASCII).trim();
236
237         logger.debug("handleIncomingMessage: {}", message);
238
239         if (READ_ERROR.equals(message)) {
240             dispatchKeyValue(KEY_ERROR, MSG_VALUE_ON);
241             return;
242         }
243
244         // Amp controller sends status string: #>1200010000130809100601
245         Matcher matcher = PATTERN.matcher(message);
246         if (matcher.find()) {
247             // pull out just the digits and send them as an event
248             dispatchKeyValue(KEY_ZONE_UPDATE, matcher.group(1));
249         } else {
250             logger.debug("no match on message: {}", message);
251         }
252     }
253
254     /**
255      * Dispatch an event (key, value) to the event listeners
256      *
257      * @param key the key
258      * @param value the value
259      */
260     private void dispatchKeyValue(String key, String value) {
261         MonopriceAudioMessageEvent event = new MonopriceAudioMessageEvent(this, key, value);
262         listeners.forEach(l -> l.onNewMessageEvent(event));
263     }
264 }