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.mpd.internal.protocol;
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;
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;
34 * Class for communicating with the music player daemon through an IP connection
36 * @author Stefan Röllin - Initial contribution
39 public class MPDConnectionThread extends Thread {
41 private static final int RECONNECTION_TIMEOUT_SEC = 60;
43 private final Logger logger = LoggerFactory.getLogger(MPDConnectionThread.class);
45 private final MPDResponseListener listener;
47 private final String address;
48 private final Integer port;
49 private final String password;
51 private @Nullable Socket socket = null;
52 private @Nullable InputStreamReader inputStreamReader = null;
53 private @Nullable BufferedReader reader = null;
55 private final List<MPDCommand> pendingCommands = new ArrayList<>();
56 private AtomicBoolean isInIdle = new AtomicBoolean(false);
57 private AtomicBoolean disposed = new AtomicBoolean(false);
59 public MPDConnectionThread(MPDResponseListener listener, String address, Integer port, String password) {
60 this.listener = listener;
61 this.address = address;
63 this.password = password;
70 while (!disposed.get()) {
72 synchronized (pendingCommands) {
73 pendingCommands.clear();
74 pendingCommands.add(new MPDCommand("status"));
75 pendingCommands.add(new MPDCommand("currentsong"));
78 establishConnection();
79 updateThingStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
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());
94 if (!disposed.get()) {
95 sleep(RECONNECTION_TIMEOUT_SEC * 1000);
98 } catch (InterruptedException ignore) {
103 * dispose the connection
105 public void dispose() {
107 Socket socket = this.socket;
108 if (socket != null) {
111 } catch (IOException ignore) {
118 * add a command to the pending commands queue
120 * @param command command to add
122 public void addCommand(MPDCommand command) {
123 insertCommand(command, -1);
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) {
131 index = pendingCommands.size();
133 pendingCommands.add(index, command);
134 sendNoIdleIfInIdle();
138 private void updateThingStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
139 if (!disposed.get()) {
140 listener.updateThingStatus(status, statusDetail, description);
144 private void sendNoIdleIfInIdle() {
145 if (isInIdle.compareAndSet(true, false)) {
147 sendCommand(new MPDCommand("noidle"));
148 } catch (IOException e) {
149 logger.debug("sendCommand(noidle) failed", e);
154 private void establishConnection() throws UnknownHostException, IOException, MPDException {
157 MPDCommand currentCommand = new MPDCommand("connect");
158 MPDResponse response = readResponse(currentCommand);
160 if (!response.isOk()) {
161 throw new MPDException("Failed to connect to " + this.address + ":" + this.port);
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");
174 private void openSocket() throws UnknownHostException, IOException, MPDException {
175 logger.debug("opening connection to {} port {}", address, port);
177 if (address.isEmpty()) {
178 throw new MPDException("The parameter 'ipAddress' is missing.");
180 if (port < 1 || port > 65335) {
181 throw new MPDException("The parameter 'port' has an invalid value.");
184 Socket socket = new Socket(address, port);
186 inputStreamReader = new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8);
187 reader = new BufferedReader(inputStreamReader);
189 this.socket = socket;
192 private void processPendingCommands() throws IOException, MPDException {
193 MPDCommand currentCommand;
195 while (!disposed.get()) {
196 synchronized (pendingCommands) {
197 if (!pendingCommands.isEmpty()) {
198 currentCommand = pendingCommands.remove(0);
200 currentCommand = new MPDCommand("idle");
203 sendCommand(currentCommand);
204 if ("idle".equals(currentCommand.getCommand())) {
209 MPDResponse response = readResponse(currentCommand);
210 if (!response.isOk()) {
211 insertCommand(new MPDCommand("clearerror"), 0);
213 listener.onResponse(response);
217 private void closeSocket() {
218 logger.debug("Closing socket");
219 BufferedReader reader = this.reader;
220 if (reader != null) {
223 } catch (IOException ignore) {
228 InputStreamReader inputStreamReader = this.inputStreamReader;
229 if (inputStreamReader != null) {
231 inputStreamReader.close();
232 } catch (IOException ignore) {
234 this.inputStreamReader = null;
237 Socket socket = this.socket;
238 if (socket != null) {
241 } catch (IOException ignore) {
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');
255 throw new IOException("Connection closed unexpectedly.");
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;
264 final BufferedReader reader = this.reader;
265 if (reader != null) {
267 String line = reader.readLine();
268 logger.trace("received line '{}'", line);
271 if (line.startsWith("ACK [4")) {
272 logger.warn("command '{}' failed with permission error '{}'", command, line);
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();
280 } else if (line.startsWith("OK")) {
283 response.addLine(line.trim());
287 throw new IOException("Communication failed unexpectedly.");
292 throw new IOException("Connection closed unexpectedly.");