2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.kaleidescape.internal.communication;
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;
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;
32 * Abstract class for communicating with the Kaleidescape component
34 * @author Laurent Garnier - Initial contribution
35 * @author Michael Lobstein - Adapted for the Kaleidescape binding
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";
43 private final Pattern pattern = Pattern.compile("^(\\d{2})/./(\\d{3})\\:([^:^/]*)\\:(.*?)\\:/(\\d{2})$");
45 private final Logger logger = LoggerFactory.getLogger(KaleidescapeConnector.class);
47 /** The output stream */
48 protected @Nullable OutputStream dataOut;
50 /** The input stream */
51 protected @Nullable InputStream dataIn;
53 /** true if the connection is established, false if not */
54 private boolean connected;
56 private @Nullable Thread readerThread;
58 private final List<KaleidescapeMessageEventListener> listeners = new ArrayList<>();
61 * Get whether the connection is established or not
63 * @return true if the connection is established
65 public boolean isConnected() {
70 * Set whether the connection is established or not
72 * @param connected true if the connection is established
74 protected void setConnected(boolean connected) {
75 this.connected = connected;
79 * Set the thread that handles the feedback messages
81 * @param readerThread the thread
83 protected void setReaderThread(Thread readerThread) {
84 this.readerThread = readerThread;
88 * Open the connection with the Kaleidescape component
90 * @throws KaleidescapeException - In case of any problem
92 public abstract void open() throws KaleidescapeException;
95 * Close the connection with the Kaleidescape component
97 public abstract void close();
100 * Stop the thread that handles the feedback messages and close the opened input and output streams
102 protected void cleanup() {
103 Thread readerThread = this.readerThread;
104 OutputStream dataOut = this.dataOut;
105 if (dataOut != null) {
108 } catch (IOException e) {
109 logger.debug("Error closing dataOut: {}", e.getMessage());
113 InputStream dataIn = this.dataIn;
114 if (dataIn != null) {
117 } catch (IOException e) {
118 logger.debug("Error closing dataIn: {}", e.getMessage());
122 if (readerThread != null) {
123 readerThread.interrupt();
124 this.readerThread = null;
126 readerThread.join(3000);
127 } catch (InterruptedException e) {
128 logger.debug("Error joining readerThread: {}", e.getMessage());
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.
137 * @param dataBuffer the buffer into which the data is read.
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.
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
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");
152 return dataIn.read(dataBuffer);
153 } catch (IOException e) {
154 throw new KaleidescapeException("readInput failed: " + e.getMessage(), e);
159 * Ping the connection by requesting the time from the component
161 * @throws KaleidescapeException - In case of any problem
163 public void ping() throws KaleidescapeException {
164 sendCommand(KaleidescapeBindingConstants.GET_TIME);
168 * Request the Kaleidescape component to execute a command
170 * @param cmd the command to execute
172 * @throws KaleidescapeException - In case of any problem
174 public void sendCommand(@Nullable String cmd) throws KaleidescapeException {
175 sendCommand(cmd, null);
179 * Request the Kaleidescape component to execute a command
181 * @param cmd the command to execute
182 * @param cachedMessage an optional cached message that will immediately be sent as a KaleidescapeMessageEvent
184 * @throws KaleidescapeException - In case of any problem
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);
195 String messageStr = BEGIN_CMD + cmd + END_CMD;
197 logger.debug("Send command {}", messageStr);
199 OutputStream dataOut = this.dataOut;
200 if (dataOut == null) {
201 throw new KaleidescapeException("Send command \"" + messageStr + "\" failed: output stream is null");
204 dataOut.write(messageStr.getBytes(StandardCharsets.US_ASCII));
206 } catch (IOException e) {
207 throw new KaleidescapeException("Send command \"" + cmd + "\" failed: " + e.getMessage(), e);
212 * Add a listener to the list of listeners to be notified with events
214 * @param listener the listener
216 public void addEventListener(KaleidescapeMessageEventListener listener) {
217 listeners.add(listener);
221 * Remove a listener from the list of listeners to be notified with events
223 * @param listener the listener
225 public void removeEventListener(KaleidescapeMessageEventListener listener) {
226 listeners.remove(listener);
230 * Analyze an incoming message and dispatch corresponding event (key, value) to the event listeners
232 * @param incomingMessage the received message
234 public void handleIncomingMessage(byte[] incomingMessage) {
235 String message = new String(incomingMessage, StandardCharsets.US_ASCII).trim();
237 // ignore empty success messages
238 if (!SUCCESS_MSG.equals(message)) {
239 logger.debug("handleIncomingMessage: {}", message);
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})$");
247 Matcher matcher = pattern.matcher(message);
248 if (matcher.find()) {
249 dispatchKeyValue(matcher.group(3), matcher.group(4), false);
251 logger.debug("no match on message: {}", message);
252 if (message.contains(KaleidescapeBindingConstants.STANDBY_MSG)) {
253 dispatchKeyValue(KaleidescapeBindingConstants.STANDBY_MSG, "", false);
260 * Dispatch an event (key, value, isCached) to the event listeners
263 * @param value the value
264 * @param isCached indicates if this event was generated from a cached value
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));