]> git.basschouten.com Git - openhab-addons.git/blob
e462a393c4116f5154c32f2c2a2c1d08b2d89a3c
[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.kaleidescape.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.kaleidescape.internal.KaleidescapeBindingConstants;
27 import org.openhab.binding.kaleidescape.internal.KaleidescapeException;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
30
31 /**
32  * Abstract class for communicating with the Kaleidescape component
33  *
34  * @author Laurent Garnier - Initial contribution
35  * @author Michael Lobstein - Adapted for the Kaleidescape binding
36  */
37 @NonNullByDefault
38 public abstract class KaleidescapeConnector {
39     private static final String SUCCESS_MSG = "01/1/000:/89";
40     private static final String BEGIN_CMD = "01/1/";
41     private static final String END_CMD = ":\r";
42
43     private final Pattern pattern = Pattern.compile("^(\\d{2})/./(\\d{3})\\:([^:^/]*)\\:(.*?)\\:/(\\d{2})$");
44
45     private final Logger logger = LoggerFactory.getLogger(KaleidescapeConnector.class);
46
47     /** The output stream */
48     protected @Nullable OutputStream dataOut;
49
50     /** The input stream */
51     protected @Nullable InputStream dataIn;
52
53     /** true if the connection is established, false if not */
54     private boolean connected;
55
56     private @Nullable Thread readerThread;
57
58     private final List<KaleidescapeMessageEventListener> listeners = new ArrayList<>();
59
60     /**
61      * Get whether the connection is established or not
62      *
63      * @return true if the connection is established
64      */
65     public boolean isConnected() {
66         return connected;
67     }
68
69     /**
70      * Set whether the connection is established or not
71      *
72      * @param connected true if the connection is established
73      */
74     protected void setConnected(boolean connected) {
75         this.connected = connected;
76     }
77
78     /**
79      * Set the thread that handles the feedback messages
80      *
81      * @param readerThread the thread
82      */
83     protected void setReaderThread(Thread readerThread) {
84         this.readerThread = readerThread;
85     }
86
87     /**
88      * Open the connection with the Kaleidescape component
89      *
90      * @throws KaleidescapeException - In case of any problem
91      */
92     public abstract void open() throws KaleidescapeException;
93
94     /**
95      * Close the connection with the Kaleidescape component
96      */
97     public abstract void close();
98
99     /**
100      * Stop the thread that handles the feedback messages and close the opened input and output streams
101      */
102     protected void cleanup() {
103         Thread readerThread = this.readerThread;
104         OutputStream dataOut = this.dataOut;
105         if (dataOut != null) {
106             try {
107                 dataOut.close();
108             } catch (IOException e) {
109                 logger.debug("Error closing dataOut: {}", e.getMessage());
110             }
111             this.dataOut = null;
112         }
113         InputStream dataIn = this.dataIn;
114         if (dataIn != null) {
115             try {
116                 dataIn.close();
117             } catch (IOException e) {
118                 logger.debug("Error closing dataIn: {}", e.getMessage());
119             }
120             this.dataIn = null;
121         }
122         if (readerThread != null) {
123             readerThread.interrupt();
124             this.readerThread = null;
125             try {
126                 readerThread.join(3000);
127             } catch (InterruptedException e) {
128                 logger.debug("Error joining readerThread: {}", e.getMessage());
129             }
130         }
131     }
132
133     /**
134      * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
135      * actually read is returned as an integer.
136      *
137      * @param dataBuffer the buffer into which the data is read.
138      *
139      * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
140      *         stream has been reached.
141      *
142      * @throws KaleidescapeException - If the input stream is null, if the first byte cannot be read for any reason
143      *             other than the end of the file, if the input stream has been closed, or if some other I/O error
144      *             occurs.
145      */
146     protected int readInput(byte[] dataBuffer) throws KaleidescapeException {
147         InputStream dataIn = this.dataIn;
148         if (dataIn == null) {
149             throw new KaleidescapeException("readInput failed: input stream is null");
150         }
151         try {
152             return dataIn.read(dataBuffer);
153         } catch (IOException e) {
154             throw new KaleidescapeException("readInput failed: " + e.getMessage(), e);
155         }
156     }
157
158     /**
159      * Ping the connection by requesting the time from the component
160      *
161      * @throws KaleidescapeException - In case of any problem
162      */
163     public void ping() throws KaleidescapeException {
164         sendCommand(KaleidescapeBindingConstants.GET_TIME);
165     }
166
167     /**
168      * Request the Kaleidescape component to execute a command
169      *
170      * @param cmd the command to execute
171      *
172      * @throws KaleidescapeException - In case of any problem
173      */
174     public void sendCommand(@Nullable String cmd) throws KaleidescapeException {
175         sendCommand(cmd, null);
176     }
177
178     /**
179      * Request the Kaleidescape component to execute a command
180      *
181      * @param cmd the command to execute
182      * @param cachedMessage an optional cached message that will immediately be sent as a KaleidescapeMessageEvent
183      *
184      * @throws KaleidescapeException - In case of any problem
185      */
186     public void sendCommand(@Nullable String cmd, @Nullable String cachedMessage) throws KaleidescapeException {
187         // if sent a cachedMessage, just send out an event with the data so KaleidescapeMessageHandler will process it
188         if (cmd != null && cachedMessage != null) {
189             logger.debug("Command: '{}' returning cached value: '{}'", cmd, cachedMessage);
190             // change GET_SOMETHING into SOMETHING and special case GET_PLAYING_TITLE_NAME into TITLE_NAME
191             dispatchKeyValue(cmd.replace("GET_", "").replace("PLAYING_TITLE_NAME", "TITLE_NAME"), cachedMessage, true);
192             return;
193         }
194
195         String messageStr = BEGIN_CMD + cmd + END_CMD;
196
197         logger.debug("Send command {}", messageStr);
198
199         OutputStream dataOut = this.dataOut;
200         if (dataOut == null) {
201             throw new KaleidescapeException("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 KaleidescapeException("Send command \"" + cmd + "\" 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(KaleidescapeMessageEventListener 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(KaleidescapeMessageEventListener listener) {
226         listeners.remove(listener);
227     }
228
229     /**
230      * Analyze an incoming message and dispatch corresponding event (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         // ignore empty success messages
238         if (!SUCCESS_MSG.equals(message)) {
239             logger.debug("handleIncomingMessage: {}", message);
240
241             // Kaleidescape message ie: 01/!/000:TITLE_NAME:Office Space:/79
242             // or: 01/!/000:PLAY_STATUS:2:0:01:07124:00138:001:00311:00138:/27
243             // or: 01/1/000:TIME:2020:04:27:11:38:52:CDT:/84
244             // g1=zoneid, g2=sequence, g3=message name, g4=message, g5=checksum
245             // pattern : "^(\\d{2})/./(\\d{3})\\:([^:^/]*)\\:(.*?)\\:/(\\d{2})$");
246
247             Matcher matcher = pattern.matcher(message);
248             if (matcher.find()) {
249                 dispatchKeyValue(matcher.group(3), matcher.group(4), false);
250             } else {
251                 logger.debug("no match on message: {}", message);
252                 if (message.contains(KaleidescapeBindingConstants.STANDBY_MSG)) {
253                     dispatchKeyValue(KaleidescapeBindingConstants.STANDBY_MSG, "", false);
254                 }
255             }
256         }
257     }
258
259     /**
260      * Dispatch an event (key, value, isCached) to the event listeners
261      *
262      * @param key the key
263      * @param value the value
264      * @param isCached indicates if this event was generated from a cached value
265      */
266     private void dispatchKeyValue(String key, String value, boolean isCached) {
267         KaleidescapeMessageEvent event = new KaleidescapeMessageEvent(this, key, value, isCached);
268         listeners.forEach(l -> l.onNewMessageEvent(event));
269     }
270 }