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.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 sendMCACCMemoryQuery() {
200 return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.MCACC_MEMORY_QUERY));
204 public boolean sendPowerCommand(Command command, int zone) throws CommandTypeNotSupportedException {
205 AvrCommand commandToSend = null;
207 if (command == OnOffType.ON) {
208 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.POWER_ON, zone);
209 // Send the first Power ON command.
210 sendCommand(commandToSend);
212 // According to the Pioneer Specs, the first request only wakeup the
213 // AVR CPU, the second one Power ON the AVR. Still according to the Pioneer Specs, the second
214 // request has to be delayed of 100 ms.
216 TimeUnit.MILLISECONDS.sleep(100);
217 } catch (InterruptedException ex) {
218 Thread.currentThread().interrupt();
220 } else if (command == OnOffType.OFF) {
221 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.POWER_OFF, zone);
223 throw new CommandTypeNotSupportedException("Command type not supported.");
226 return sendCommand(commandToSend);
230 public boolean sendVolumeCommand(Command command, int zone) throws CommandTypeNotSupportedException {
231 boolean commandSent = false;
233 // The OnOffType for volume is equal to the Mute command
234 if (command instanceof OnOffType) {
235 commandSent = sendMuteCommand(command, zone);
237 AvrCommand commandToSend = null;
239 if (command == IncreaseDecreaseType.DECREASE) {
240 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.VOLUME_DOWN, zone);
241 } else if (command == IncreaseDecreaseType.INCREASE) {
242 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.VOLUME_UP, zone);
243 } else if (command instanceof PercentType) {
244 String ipControlVolume = VolumeConverter
245 .convertFromPercentToIpControlVolume(((PercentType) command).doubleValue(), zone);
246 commandToSend = RequestResponseFactory.getIpControlCommand(ParameterizedCommandType.VOLUME_SET, zone)
247 .setParameter(ipControlVolume);
248 } else if (command instanceof DecimalType) {
249 String ipControlVolume = VolumeConverter
250 .convertFromDbToIpControlVolume(((DecimalType) command).doubleValue(), zone);
251 commandToSend = RequestResponseFactory.getIpControlCommand(ParameterizedCommandType.VOLUME_SET, zone)
252 .setParameter(ipControlVolume);
254 throw new CommandTypeNotSupportedException("Command type not supported.");
257 commandSent = sendCommand(commandToSend);
263 public boolean sendInputSourceCommand(Command command, int zone) throws CommandTypeNotSupportedException {
264 AvrCommand commandToSend = null;
266 if (command == IncreaseDecreaseType.INCREASE) {
267 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.INPUT_CHANGE_CYCLIC, zone);
268 } else if (command == IncreaseDecreaseType.DECREASE) {
269 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.INPUT_CHANGE_REVERSE, zone);
270 } else if (command instanceof StringType) {
271 String inputSourceValue = ((StringType) command).toString();
272 commandToSend = RequestResponseFactory.getIpControlCommand(ParameterizedCommandType.INPUT_CHANNEL_SET, zone)
273 .setParameter(inputSourceValue);
275 throw new CommandTypeNotSupportedException("Command type not supported.");
278 return sendCommand(commandToSend);
282 public boolean sendListeningModeCommand(Command command, int zone) throws CommandTypeNotSupportedException {
283 AvrCommand commandToSend = null;
285 if (command == IncreaseDecreaseType.INCREASE) {
286 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.LISTENING_MODE_CHANGE_CYCLIC,
288 } else if (command instanceof StringType) {
289 String listeningModeValue = ((StringType) command).toString();
290 commandToSend = RequestResponseFactory
291 .getIpControlCommand(ParameterizedCommandType.LISTENING_MODE_SET, zone)
292 .setParameter(listeningModeValue);
294 throw new CommandTypeNotSupportedException("Command type not supported.");
297 return sendCommand(commandToSend);
301 public boolean sendMuteCommand(Command command, int zone) throws CommandTypeNotSupportedException {
302 AvrCommand commandToSend = null;
304 if (command == OnOffType.ON) {
305 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.MUTE_ON, zone);
306 } else if (command == OnOffType.OFF) {
307 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.MUTE_OFF, zone);
309 throw new CommandTypeNotSupportedException("Command type not supported.");
312 return sendCommand(commandToSend);
316 public boolean sendMCACCMemoryCommand(Command command) throws CommandTypeNotSupportedException {
317 AvrCommand commandToSend = null;
319 if (command == IncreaseDecreaseType.INCREASE) {
320 commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.MCACC_MEMORY_CHANGE_CYCLIC);
321 } else if (command instanceof StringType) {
322 String MCACCMemoryValue = ((StringType) command).toString();
323 commandToSend = RequestResponseFactory.getIpControlCommand(ParameterizedCommandType.MCACC_MEMORY_SET)
324 .setParameter(MCACCMemoryValue);
326 throw new CommandTypeNotSupportedException("Command type not supported.");
329 return sendCommand(commandToSend);
333 * Read incoming data from the AVR and notify listeners for dataReceived and disconnection.
335 * @author Antoine Besnard
338 private class IpControlInputStreamReader extends Thread {
340 private BufferedReader bufferedReader;
342 private volatile boolean stopReader;
344 // This latch is used to block the stop method until the reader is really stopped.
345 private CountDownLatch stopLatch;
348 * Construct a reader that read the given inputStream
350 * @param ipControlSocket
351 * @throws IOException
353 public IpControlInputStreamReader(InputStream inputStream) {
354 this.bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
355 this.stopLatch = new CountDownLatch(1);
357 this.setDaemon(true);
358 this.setName("IpControlInputStreamReader-" + getConnectionName());
364 while (!stopReader && !Thread.currentThread().isInterrupted()) {
365 String receivedData = null;
367 receivedData = bufferedReader.readLine();
368 } catch (SocketTimeoutException e) {
369 // Do nothing. Just happen to allow the thread to check if it has to stop.
372 if (receivedData != null) {
373 logger.debug("Data received from AVR @{}: {}", getConnectionName(), receivedData);
374 AvrStatusUpdateEvent event = new AvrStatusUpdateEvent(StreamAvrConnection.this, receivedData);
375 synchronized (updateListeners) {
376 for (AvrUpdateListener pioneerAvrEventListener : updateListeners) {
377 pioneerAvrEventListener.statusUpdateReceived(event);
382 } catch (IOException e) {
383 logger.warn("The AVR @{} is disconnected.", getConnectionName(), e);
384 AvrDisconnectionEvent event = new AvrDisconnectionEvent(StreamAvrConnection.this, e);
385 for (AvrDisconnectionListener pioneerAvrDisconnectionListener : disconnectionListeners) {
386 pioneerAvrDisconnectionListener.onDisconnection(event);
390 // Notify the stopReader method caller that the reader is stopped.
391 this.stopLatch.countDown();
395 * Stop this reader. Block until the reader is really stopped.
397 public void stopReader() {
398 this.stopReader = true;
400 this.stopLatch.await(READ_TIMEOUT, TimeUnit.MILLISECONDS);
401 } catch (InterruptedException e) {
402 // Do nothing. The timeout is just here for safety and to be sure that the call to this method will not
403 // block the caller indefinitely.
404 // This exception should never happen.