]> git.basschouten.com Git - openhab-addons.git/blob
c94dd7cd26eef8639edcc9b6550c67ec8893162a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.mpd.internal.protocol;
14
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.net.Socket;
19 import java.net.UnknownHostException;
20 import java.nio.charset.StandardCharsets;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.concurrent.atomic.AtomicBoolean;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.mpd.internal.MPDException;
28 import org.openhab.core.thing.ThingStatus;
29 import org.openhab.core.thing.ThingStatusDetail;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 /**
34  * Class for communicating with the music player daemon through an IP connection
35  *
36  * @author Stefan Röllin - Initial contribution
37  */
38 @NonNullByDefault
39 public class MPDConnectionThread extends Thread {
40
41     private static final int RECONNECTION_TIMEOUT_SEC = 60;
42
43     private final Logger logger = LoggerFactory.getLogger(MPDConnectionThread.class);
44
45     private final MPDResponseListener listener;
46
47     private final String address;
48     private final Integer port;
49     private final String password;
50
51     private @Nullable Socket socket = null;
52     private @Nullable InputStreamReader inputStreamReader = null;
53     private @Nullable BufferedReader reader = null;
54
55     private final List<MPDCommand> pendingCommands = new ArrayList<>();
56     private AtomicBoolean isInIdle = new AtomicBoolean(false);
57     private AtomicBoolean disposed = new AtomicBoolean(false);
58
59     public MPDConnectionThread(MPDResponseListener listener, String address, Integer port, String password) {
60         this.listener = listener;
61         this.address = address;
62         this.port = port;
63         this.password = password;
64         setDaemon(true);
65     }
66
67     @Override
68     public void run() {
69         try {
70             while (!disposed.get()) {
71                 try {
72                     synchronized (pendingCommands) {
73                         pendingCommands.clear();
74                         pendingCommands.add(new MPDCommand("status"));
75                         pendingCommands.add(new MPDCommand("currentsong"));
76                     }
77
78                     establishConnection();
79                     updateThingStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
80
81                     processPendingCommands();
82                 } catch (UnknownHostException e) {
83                     updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
84                             "Unknown host " + address);
85                 } catch (IOException e) {
86                     updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
87                 } catch (MPDException e) {
88                     updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
89                 }
90
91                 isInIdle.set(false);
92                 closeSocket();
93
94                 if (!disposed.get()) {
95                     sleep(RECONNECTION_TIMEOUT_SEC * 1000);
96                 }
97             }
98         } catch (InterruptedException ignore) {
99         }
100     }
101
102     /**
103      * dispose the connection
104      */
105     public void dispose() {
106         disposed.set(true);
107         Socket socket = this.socket;
108         if (socket != null) {
109             try {
110                 socket.close();
111             } catch (IOException ignore) {
112             }
113             this.socket = null;
114         }
115     }
116
117     /**
118      * add a command to the pending commands queue
119      *
120      * @param command command to add
121      */
122     public void addCommand(MPDCommand command) {
123         insertCommand(command, -1);
124     }
125
126     private void insertCommand(MPDCommand command, int position) {
127         logger.debug("insert command '{}' at position {}", command.getCommand(), position);
128         int index = position;
129         synchronized (pendingCommands) {
130             if (index < 0) {
131                 index = pendingCommands.size();
132             }
133             pendingCommands.add(index, command);
134             sendNoIdleIfInIdle();
135         }
136     }
137
138     private void updateThingStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
139         if (!disposed.get()) {
140             listener.updateThingStatus(status, statusDetail, description);
141         }
142     }
143
144     private void sendNoIdleIfInIdle() {
145         if (isInIdle.compareAndSet(true, false)) {
146             try {
147                 sendCommand(new MPDCommand("noidle"));
148             } catch (IOException e) {
149                 logger.debug("sendCommand(noidle) failed", e);
150             }
151         }
152     }
153
154     private void establishConnection() throws UnknownHostException, IOException, MPDException {
155         openSocket();
156
157         MPDCommand currentCommand = new MPDCommand("connect");
158         MPDResponse response = readResponse(currentCommand);
159
160         if (!response.isOk()) {
161             throw new MPDException("Failed to connect to " + this.address + ":" + this.port);
162         }
163
164         if (!password.isEmpty()) {
165             currentCommand = new MPDCommand("password", password);
166             sendCommand(currentCommand);
167             response = readResponse(currentCommand);
168             if (!response.isOk()) {
169                 throw new MPDException("Could not authenticate, please validate your password");
170             }
171         }
172     }
173
174     private void openSocket() throws UnknownHostException, IOException, MPDException {
175         logger.debug("opening connection to {} port {}", address, port);
176
177         if (address.isEmpty()) {
178             throw new MPDException("The parameter 'ipAddress' is missing.");
179         }
180         if (port < 1 || port > 65335) {
181             throw new MPDException("The parameter 'port' has an invalid value.");
182         }
183
184         Socket socket = new Socket(address, port);
185
186         inputStreamReader = new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8);
187         reader = new BufferedReader(inputStreamReader);
188
189         this.socket = socket;
190     }
191
192     private void processPendingCommands() throws IOException, MPDException {
193         MPDCommand currentCommand;
194
195         while (!disposed.get()) {
196             synchronized (pendingCommands) {
197                 if (!pendingCommands.isEmpty()) {
198                     currentCommand = pendingCommands.remove(0);
199                 } else {
200                     currentCommand = new MPDCommand("idle");
201                 }
202
203                 sendCommand(currentCommand);
204                 if ("idle".equals(currentCommand.getCommand())) {
205                     isInIdle.set(true);
206                 }
207             }
208
209             MPDResponse response = readResponse(currentCommand);
210             if (!response.isOk()) {
211                 insertCommand(new MPDCommand("clearerror"), 0);
212             }
213             listener.onResponse(response);
214         }
215     }
216
217     private void closeSocket() {
218         logger.debug("Closing socket");
219         BufferedReader reader = this.reader;
220         if (reader != null) {
221             try {
222                 reader.close();
223             } catch (IOException ignore) {
224             }
225             this.reader = null;
226         }
227
228         InputStreamReader inputStreamReader = this.inputStreamReader;
229         if (inputStreamReader != null) {
230             try {
231                 inputStreamReader.close();
232             } catch (IOException ignore) {
233             }
234             this.inputStreamReader = null;
235         }
236
237         Socket socket = this.socket;
238         if (socket != null) {
239             try {
240                 socket.close();
241             } catch (IOException ignore) {
242             }
243             this.socket = null;
244         }
245     }
246
247     private void sendCommand(MPDCommand command) throws IOException {
248         logger.trace("send command '{}'", command);
249         final Socket socket = this.socket;
250         if (socket != null) {
251             String line = command.asLine();
252             socket.getOutputStream().write(line.getBytes(StandardCharsets.UTF_8));
253             socket.getOutputStream().write('\n');
254         } else {
255             throw new IOException("Connection closed unexpectedly.");
256         }
257     }
258
259     private MPDResponse readResponse(MPDCommand command) throws IOException, MPDException {
260         logger.trace("read response for command '{}'", command.getCommand());
261         MPDResponse response = new MPDResponse(command.getCommand());
262         boolean done = false;
263
264         final BufferedReader reader = this.reader;
265         if (reader != null) {
266             while (!done) {
267                 String line = reader.readLine();
268                 logger.trace("received line '{}'", line);
269
270                 if (line != null) {
271                     if (line.startsWith("ACK [4")) {
272                         logger.warn("command '{}' failed with permission error '{}'", command, line);
273                         isInIdle.set(false);
274                         throw new MPDException(
275                                 "Please validate your password and/or your permissions on the Music Player Daemon.");
276                     } else if (line.startsWith("ACK")) {
277                         logger.warn("command '{}' failed with '{}'", command, line);
278                         response.setFailed();
279                         done = true;
280                     } else if (line.startsWith("OK")) {
281                         done = true;
282                     } else {
283                         response.addLine(line.trim());
284                     }
285                 } else {
286                     isInIdle.set(false);
287                     throw new IOException("Communication failed unexpectedly.");
288                 }
289             }
290         } else {
291             isInIdle.set(false);
292             throw new IOException("Connection closed unexpectedly.");
293         }
294
295         isInIdle.set(false);
296
297         return response;
298     }
299 }