2 * Copyright (c) 2010-2020 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.pioneeravr.internal.protocol;
15 import java.io.BufferedReader;
16 import java.io.DataOutputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.InputStreamReader;
20 import java.io.OutputStream;
21 import java.net.SocketTimeoutException;
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.concurrent.CountDownLatch;
25 import java.util.concurrent.TimeUnit;
27 import org.openhab.binding.pioneeravr.internal.protocol.ParameterizedCommand.ParameterizedCommandType;
28 import org.openhab.binding.pioneeravr.internal.protocol.SimpleCommand.SimpleCommandType;
29 import org.openhab.binding.pioneeravr.internal.protocol.avr.AvrCommand;
30 import org.openhab.binding.pioneeravr.internal.protocol.avr.AvrConnection;
31 import org.openhab.binding.pioneeravr.internal.protocol.avr.CommandTypeNotSupportedException;
32 import org.openhab.binding.pioneeravr.internal.protocol.event.AvrDisconnectionEvent;
33 import org.openhab.binding.pioneeravr.internal.protocol.event.AvrDisconnectionListener;
34 import org.openhab.binding.pioneeravr.internal.protocol.event.AvrStatusUpdateEvent;
35 import org.openhab.binding.pioneeravr.internal.protocol.event.AvrUpdateListener;
36 import org.openhab.binding.pioneeravr.internal.protocol.utils.VolumeConverter;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.IncreaseDecreaseType;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.PercentType;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.util.HexUtils;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
49 * A class that wraps the communication to Pioneer AVR devices by using Input/Ouptut streams.
51 * see {@link http ://www.pioneerelectronics.com/StaticFiles/PUSA/Files/Home%20Custom %20Install/VSX-1120-K-RS232.PDF}
52 * for the protocol specs
54 * Based on the Onkyo binding by Pauli Anttila and others.
56 * @author Antoine Besnard - Initial contribution
57 * @author Rainer Ostendorf - Initial contribution
58 * @author Leroy Foerster - Listening Mode, Playing Listening Mode
60 public abstract class StreamAvrConnection implements AvrConnection {
62 private final Logger logger = LoggerFactory.getLogger(StreamAvrConnection.class);
64 // The maximum time to wait incoming messages.
65 private static final Integer READ_TIMEOUT = 1000;
67 private List<AvrUpdateListener> updateListeners;
68 private List<AvrDisconnectionListener> disconnectionListeners;
70 private IpControlInputStreamReader inputStreamReader;
71 private DataOutputStream outputStream;
73 public StreamAvrConnection() {
74 this.updateListeners = new ArrayList<>();
75 this.disconnectionListeners = new ArrayList<>();
79 public void addUpdateListener(AvrUpdateListener listener) {
80 synchronized (updateListeners) {
81 updateListeners.add(listener);
86 public void addDisconnectionListener(AvrDisconnectionListener listener) {
87 synchronized (disconnectionListeners) {
88 disconnectionListeners.add(listener);
93 public boolean connect() {
98 // Start the inputStream reader.
99 inputStreamReader = new IpControlInputStreamReader(getInputStream());
100 inputStreamReader.start();
103 outputStream = new DataOutputStream(getOutputStream());
104 } catch (IOException ioException) {
105 logger.debug("Can't connect to {}. Cause: {}", getConnectionName(), ioException.getMessage());
108 return isConnected();
112 * Open the connection to the AVR.
114 * @throws IOException
116 protected abstract void openConnection() throws IOException;
119 * Return the inputStream to read responses.
122 * @throws IOException
124 protected abstract InputStream getInputStream() throws IOException;
127 * Return the outputStream to send commands.
130 * @throws IOException
132 protected abstract OutputStream getOutputStream() throws IOException;
135 public void close() {
136 if (inputStreamReader != null) {
137 // This method block until the reader is really stopped.
138 inputStreamReader.stopReader();
139 inputStreamReader = null;
140 logger.debug("Stream reader stopped!");
145 * Sends to command to the receiver. It does not wait for a reply.
147 * @param ipControlCommand
148 * the command to send.
150 protected boolean sendCommand(AvrCommand ipControlCommand) {
151 boolean isSent = false;
153 String command = ipControlCommand.getCommand();
155 if (logger.isTraceEnabled()) {
156 logger.trace("Sending {} bytes: {}", command.length(), HexUtils.bytesToHex(command.getBytes()));
158 outputStream.writeBytes(command);
159 outputStream.flush();
161 } catch (IOException ioException) {
162 logger.error("Error occurred when sending command", ioException);
163 // If an error occurs, close the connection
167 logger.debug("Command sent to AVR @{}: {}", getConnectionName(), command);
174 public boolean sendPowerQuery(int zone) {
175 return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.POWER_QUERY, zone));
179 public boolean sendVolumeQuery(int zone) {
180 return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.VOLUME_QUERY, zone));
184 public boolean sendMuteQuery(int zone) {
185 return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.MUTE_QUERY, zone));
189 public boolean sendInputSourceQuery(int zone) {
190 return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.INPUT_QUERY, zone));
194 public boolean sendListeningModeQuery(int zone) {
195 return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.LISTENING_MODE_QUERY, zone));
199 public boolean sendPowerCommand(Command command, int zone) throws CommandTypeNotSupportedException {
200 AvrCommand commandToSend = null;
202 if (command == OnOffType.ON) {
203 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.POWER_ON, zone);
204 // Send the first Power ON command.
205 sendCommand(commandToSend);
207 // According to the Pioneer Specs, the first request only wakeup the
208 // AVR CPU, the second one Power ON the AVR. Still according to the Pioneer Specs, the second
209 // request has to be delayed of 100 ms.
211 TimeUnit.MILLISECONDS.sleep(100);
212 } catch (InterruptedException ex) {
213 Thread.currentThread().interrupt();
215 } else if (command == OnOffType.OFF) {
216 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.POWER_OFF, zone);
218 throw new CommandTypeNotSupportedException("Command type not supported.");
221 return sendCommand(commandToSend);
225 public boolean sendVolumeCommand(Command command, int zone) throws CommandTypeNotSupportedException {
226 boolean commandSent = false;
228 // The OnOffType for volume is equal to the Mute command
229 if (command instanceof OnOffType) {
230 commandSent = sendMuteCommand(command, zone);
232 AvrCommand commandToSend = null;
234 if (command == IncreaseDecreaseType.DECREASE) {
235 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.VOLUME_DOWN, zone);
236 } else if (command == IncreaseDecreaseType.INCREASE) {
237 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.VOLUME_UP, zone);
238 } else if (command instanceof PercentType) {
239 String ipControlVolume = VolumeConverter
240 .convertFromPercentToIpControlVolume(((PercentType) command).doubleValue(), zone);
241 commandToSend = RequestResponseFactory.getIpControlCommand(ParameterizedCommandType.VOLUME_SET, zone)
242 .setParameter(ipControlVolume);
243 } else if (command instanceof DecimalType) {
244 String ipControlVolume = VolumeConverter
245 .convertFromDbToIpControlVolume(((DecimalType) command).doubleValue(), zone);
246 commandToSend = RequestResponseFactory.getIpControlCommand(ParameterizedCommandType.VOLUME_SET, zone)
247 .setParameter(ipControlVolume);
249 throw new CommandTypeNotSupportedException("Command type not supported.");
252 commandSent = sendCommand(commandToSend);
258 public boolean sendInputSourceCommand(Command command, int zone) throws CommandTypeNotSupportedException {
259 AvrCommand commandToSend = null;
261 if (command == IncreaseDecreaseType.INCREASE) {
262 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.INPUT_CHANGE_CYCLIC, zone);
263 } else if (command == IncreaseDecreaseType.DECREASE) {
264 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.INPUT_CHANGE_REVERSE, zone);
265 } else if (command instanceof StringType) {
266 String inputSourceValue = ((StringType) command).toString();
267 commandToSend = RequestResponseFactory.getIpControlCommand(ParameterizedCommandType.INPUT_CHANNEL_SET, zone)
268 .setParameter(inputSourceValue);
270 throw new CommandTypeNotSupportedException("Command type not supported.");
273 return sendCommand(commandToSend);
277 public boolean sendListeningModeCommand(Command command, int zone) throws CommandTypeNotSupportedException {
278 AvrCommand commandToSend = null;
280 if (command == IncreaseDecreaseType.INCREASE) {
281 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.LISTENING_MODE_CHANGE_CYCLIC,
283 } else if (command instanceof StringType) {
284 String listeningModeValue = ((StringType) command).toString();
285 commandToSend = RequestResponseFactory
286 .getIpControlCommand(ParameterizedCommandType.LISTENING_MODE_SET, zone)
287 .setParameter(listeningModeValue);
289 throw new CommandTypeNotSupportedException("Command type not supported.");
292 return sendCommand(commandToSend);
296 public boolean sendMuteCommand(Command command, int zone) throws CommandTypeNotSupportedException {
297 AvrCommand commandToSend = null;
299 if (command == OnOffType.ON) {
300 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.MUTE_ON, zone);
301 } else if (command == OnOffType.OFF) {
302 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.MUTE_OFF, zone);
304 throw new CommandTypeNotSupportedException("Command type not supported.");
307 return sendCommand(commandToSend);
311 * Read incoming data from the AVR and notify listeners for dataReceived and disconnection.
313 * @author Antoine Besnard
316 private class IpControlInputStreamReader extends Thread {
318 private BufferedReader bufferedReader;
320 private volatile boolean stopReader;
322 // This latch is used to block the stop method until the reader is really stopped.
323 private CountDownLatch stopLatch;
326 * Construct a reader that read the given inputStream
328 * @param ipControlSocket
329 * @throws IOException
331 public IpControlInputStreamReader(InputStream inputStream) {
332 this.bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
333 this.stopLatch = new CountDownLatch(1);
335 this.setDaemon(true);
336 this.setName("IpControlInputStreamReader-" + getConnectionName());
342 while (!stopReader && !Thread.currentThread().isInterrupted()) {
343 String receivedData = null;
345 receivedData = bufferedReader.readLine();
346 } catch (SocketTimeoutException e) {
347 // Do nothing. Just happen to allow the thread to check if it has to stop.
350 if (receivedData != null) {
351 logger.debug("Data received from AVR @{}: {}", getConnectionName(), receivedData);
352 AvrStatusUpdateEvent event = new AvrStatusUpdateEvent(StreamAvrConnection.this, receivedData);
353 synchronized (updateListeners) {
354 for (AvrUpdateListener pioneerAvrEventListener : updateListeners) {
355 pioneerAvrEventListener.statusUpdateReceived(event);
360 } catch (IOException e) {
361 logger.warn("The AVR @{} is disconnected.", getConnectionName(), e);
362 AvrDisconnectionEvent event = new AvrDisconnectionEvent(StreamAvrConnection.this, e);
363 for (AvrDisconnectionListener pioneerAvrDisconnectionListener : disconnectionListeners) {
364 pioneerAvrDisconnectionListener.onDisconnection(event);
368 // Notify the stopReader method caller that the reader is stopped.
369 this.stopLatch.countDown();
373 * Stop this reader. Block until the reader is really stopped.
375 public void stopReader() {
376 this.stopReader = true;
378 this.stopLatch.await(READ_TIMEOUT, TimeUnit.MILLISECONDS);
379 } catch (InterruptedException e) {
380 // Do nothing. The timeout is just here for safety and to be sure that the call to this method will not
381 // block the caller indefinitely.
382 // This exception should never happen.