]> git.basschouten.com Git - openhab-addons.git/blob
b9de2f2c3cd50c236fe952266c832d8dda69f392
[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.oppo.internal.communication;
14
15 import static org.openhab.binding.oppo.internal.OppoBindingConstants.*;
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.oppo.internal.OppoException;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31
32 /**
33  * Abstract class for communicating with the Oppo player
34  *
35  * @author Laurent Garnier - Initial contribution
36  * @author Michael Lobstein - Adapted for the Oppo binding
37  */
38 @NonNullByDefault
39 public abstract class OppoConnector {
40     private static final Pattern QRY_PATTERN = Pattern.compile("^@(Q[A-Z0-9]{2}|VUP|VDN) OK (.*)$");
41     private static final Pattern STUS_PATTERN = Pattern.compile("^@(U[A-Z0-9]{2}) (.*)$");
42
43     private static final String OK_ON = "@OK ON";
44     private static final String OK_OFF = "@OK OFF";
45     private static final String NOP_OK = "@NOP OK";
46     private static final String NOP = "NOP";
47     private static final String OK = "OK";
48
49     private final Logger logger = LoggerFactory.getLogger(OppoConnector.class);
50
51     private String beginCmd = "#";
52     private String endCmd = "\r";
53
54     /** The output stream */
55     protected @Nullable OutputStream dataOut;
56
57     /** The input stream */
58     protected @Nullable InputStream dataIn;
59
60     /** true if the connection is established, false if not */
61     private boolean connected;
62
63     private @Nullable Thread readerThread;
64
65     private final List<OppoMessageEventListener> listeners = new ArrayList<>();
66
67     /**
68      * Called when using direct IP connection for 83/93/95/103/105
69      * overrides the command message preamble and removes the CR at the end
70      */
71     public void overrideCmdPreamble(boolean override) {
72         if (override) {
73             this.beginCmd = "REMOTE ";
74             this.endCmd = "";
75         }
76     }
77
78     /**
79      * Get whether the connection is established or not
80      *
81      * @return true if the connection is established
82      */
83     public boolean isConnected() {
84         return connected;
85     }
86
87     /**
88      * Set whether the connection is established or not
89      *
90      * @param connected true if the connection is established
91      */
92     protected void setConnected(boolean connected) {
93         this.connected = connected;
94     }
95
96     /**
97      * Set the thread that handles the feedback messages
98      *
99      * @param readerThread the thread
100      */
101     protected void setReaderThread(Thread readerThread) {
102         this.readerThread = readerThread;
103     }
104
105     /**
106      * Open the connection with the Oppo player
107      *
108      * @throws OppoException - In case of any problem
109      */
110     public abstract void open() throws OppoException;
111
112     /**
113      * Close the connection with the Oppo player
114      */
115     public abstract void close();
116
117     /**
118      * Stop the thread that handles the feedback messages and close the opened input and output streams
119      */
120     protected void cleanup() {
121         Thread readerThread = this.readerThread;
122         OutputStream dataOut = this.dataOut;
123         if (dataOut != null) {
124             try {
125                 dataOut.close();
126             } catch (IOException e) {
127                 logger.debug("Error closing dataOut: {}", e.getMessage());
128             }
129             this.dataOut = null;
130         }
131         InputStream dataIn = this.dataIn;
132         if (dataIn != null) {
133             try {
134                 dataIn.close();
135             } catch (IOException e) {
136                 logger.debug("Error closing dataIn: {}", e.getMessage());
137             }
138             this.dataIn = null;
139         }
140         if (readerThread != null) {
141             readerThread.interrupt();
142             this.readerThread = null;
143             try {
144                 readerThread.join(3000);
145             } catch (InterruptedException e) {
146                 logger.warn("Error joining readerThread: {}", e.getMessage());
147             }
148         }
149     }
150
151     /**
152      * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
153      * actually read is returned as an integer.
154      *
155      * @param dataBuffer the buffer into which the data is read.
156      *
157      * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
158      *         stream has been reached.
159      *
160      * @throws OppoException - If the input stream is null, if the first byte cannot be read for any reason
161      *             other than the end of the file, if the input stream has been closed, or if some other I/O error
162      *             occurs.
163      */
164     protected int readInput(byte[] dataBuffer) throws OppoException {
165         InputStream dataIn = this.dataIn;
166         if (dataIn == null) {
167             throw new OppoException("readInput failed: input stream is null");
168         }
169         try {
170             return dataIn.read(dataBuffer);
171         } catch (IOException e) {
172             throw new OppoException("readInput failed: " + e.getMessage(), e);
173         }
174     }
175
176     /**
177      * Request the Oppo controller to execute a command and pass in a value
178      *
179      * @param cmd the command to execute
180      * @param value the string value to pass with the command
181      *
182      * @throws OppoException - In case of any problem
183      */
184     public void sendCommand(OppoCommand cmd, @Nullable String value) throws OppoException {
185         sendCommand(cmd.getValue() + " " + value);
186     }
187
188     /**
189      * Request the Oppo controller to execute a command that does not specify a value
190      *
191      * @param cmd the command to execute
192      *
193      * @throws OppoException - In case of any problem
194      */
195     public void sendCommand(OppoCommand cmd) throws OppoException {
196         sendCommand(cmd.getValue());
197     }
198
199     /**
200      * Request the Oppo controller to execute a raw command string
201      *
202      * @param command the command string to run
203      *
204      * @throws OppoException - In case of any problem
205      */
206     public void sendCommand(String command) throws OppoException {
207         String messageStr = beginCmd + command + endCmd;
208         logger.debug("Sending command: {}", messageStr);
209
210         OutputStream dataOut = this.dataOut;
211         if (dataOut == null) {
212             throw new OppoException("Send command \"" + messageStr + "\" failed: output stream is null");
213         }
214         try {
215             dataOut.write(messageStr.getBytes(StandardCharsets.US_ASCII));
216             dataOut.flush();
217         } catch (IOException e) {
218             logger.debug("Send command \"{}\" failed: {}", messageStr, e.getMessage());
219             throw new OppoException("Send command \"" + command + "\" failed: " + e.getMessage(), e);
220         }
221     }
222
223     /**
224      * Add a listener to the list of listeners to be notified with events
225      *
226      * @param listener the listener
227      */
228     public void addEventListener(OppoMessageEventListener listener) {
229         listeners.add(listener);
230     }
231
232     /**
233      * Remove a listener from the list of listeners to be notified with events
234      *
235      * @param listener the listener
236      */
237     public void removeEventListener(OppoMessageEventListener listener) {
238         listeners.remove(listener);
239     }
240
241     /**
242      * Analyze an incoming message and dispatch corresponding (key, value) to the event listeners
243      *
244      * @param incomingMessage the received message
245      */
246     public void handleIncomingMessage(byte[] incomingMessage) {
247         String message = new String(incomingMessage, StandardCharsets.US_ASCII).trim();
248
249         logger.debug("handleIncomingMessage: {}", message);
250
251         if (NOP_OK.equals(message)) {
252             dispatchKeyValue(NOP, OK);
253             return;
254         }
255
256         // Before verbose mode 2 & 3 get set, these are the responses to QPW
257         if (OK_ON.equals(message)) {
258             dispatchKeyValue(QPW, ON);
259             return;
260         }
261
262         if (OK_OFF.equals(message)) {
263             dispatchKeyValue(QPW, OFF);
264             return;
265         }
266
267         // Player sent an OK response to a query: @QDT OK DVD-VIDEO or a volume update @VUP OK 100
268         Matcher matcher = QRY_PATTERN.matcher(message);
269         if (matcher.find()) {
270             // pull out the inquiry type and the remainder of the message
271             dispatchKeyValue(matcher.group(1), matcher.group(2));
272             return;
273         }
274
275         // Player sent a status update ie: @UTC 000 000 T 00:00:01
276         matcher = STUS_PATTERN.matcher(message);
277         if (matcher.find()) {
278             // pull out the update type and the remainder of the message
279             dispatchKeyValue(matcher.group(1), matcher.group(2));
280             return;
281         }
282
283         logger.debug("unhandled message: {}", message);
284     }
285
286     /**
287      * Dispatch an event (key, value) to the event listeners
288      *
289      * @param key the key
290      * @param value the value
291      */
292     private void dispatchKeyValue(String key, String value) {
293         OppoMessageEvent event = new OppoMessageEvent(this, key, value);
294         listeners.forEach(l -> l.onNewMessageEvent(event));
295     }
296 }