]> git.basschouten.com Git - openhab-addons.git/blob
8b511c46296b4901c8d42d2e24b5e96a42e6b2b4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.pioneeravr.internal.protocol;
14
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;
26
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;
46
47 /**
48  *
49  * A class that wraps the communication to Pioneer AVR devices by using Input/Ouptut streams.
50  *
51  * see {@link http ://www.pioneerelectronics.com/StaticFiles/PUSA/Files/Home%20Custom %20Install/VSX-1120-K-RS232.PDF}
52  * for the protocol specs
53  *
54  * Based on the Onkyo binding by Pauli Anttila and others.
55  *
56  * @author Antoine Besnard - Initial contribution
57  * @author Rainer Ostendorf - Initial contribution
58  * @author Leroy Foerster - Listening Mode, Playing Listening Mode
59  */
60 public abstract class StreamAvrConnection implements AvrConnection {
61
62     private final Logger logger = LoggerFactory.getLogger(StreamAvrConnection.class);
63
64     // The maximum time to wait incoming messages.
65     private static final Integer READ_TIMEOUT = 1000;
66
67     private List<AvrUpdateListener> updateListeners;
68     private List<AvrDisconnectionListener> disconnectionListeners;
69
70     private IpControlInputStreamReader inputStreamReader;
71     private DataOutputStream outputStream;
72
73     public StreamAvrConnection() {
74         this.updateListeners = new ArrayList<>();
75         this.disconnectionListeners = new ArrayList<>();
76     }
77
78     @Override
79     public void addUpdateListener(AvrUpdateListener listener) {
80         synchronized (updateListeners) {
81             updateListeners.add(listener);
82         }
83     }
84
85     @Override
86     public void addDisconnectionListener(AvrDisconnectionListener listener) {
87         synchronized (disconnectionListeners) {
88             disconnectionListeners.add(listener);
89         }
90     }
91
92     @Override
93     public boolean connect() {
94         if (!isConnected()) {
95             try {
96                 openConnection();
97
98                 // Start the inputStream reader.
99                 inputStreamReader = new IpControlInputStreamReader(getInputStream());
100                 inputStreamReader.start();
101
102                 // Get Output stream
103                 outputStream = new DataOutputStream(getOutputStream());
104             } catch (IOException ioException) {
105                 logger.debug("Can't connect to {}. Cause: {}", getConnectionName(), ioException.getMessage());
106             }
107         }
108         return isConnected();
109     }
110
111     /**
112      * Open the connection to the AVR.
113      *
114      * @throws IOException
115      */
116     protected abstract void openConnection() throws IOException;
117
118     /**
119      * Return the inputStream to read responses.
120      *
121      * @return
122      * @throws IOException
123      */
124     protected abstract InputStream getInputStream() throws IOException;
125
126     /**
127      * Return the outputStream to send commands.
128      *
129      * @return
130      * @throws IOException
131      */
132     protected abstract OutputStream getOutputStream() throws IOException;
133
134     @Override
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!");
141         }
142     }
143
144     /**
145      * Sends to command to the receiver. It does not wait for a reply.
146      *
147      * @param ipControlCommand
148      *            the command to send.
149      **/
150     protected boolean sendCommand(AvrCommand ipControlCommand) {
151         boolean isSent = false;
152         if (connect()) {
153             String command = ipControlCommand.getCommand();
154             try {
155                 if (logger.isTraceEnabled()) {
156                     logger.trace("Sending {} bytes: {}", command.length(), HexUtils.bytesToHex(command.getBytes()));
157                 }
158                 outputStream.writeBytes(command);
159                 outputStream.flush();
160                 isSent = true;
161             } catch (IOException ioException) {
162                 logger.error("Error occurred when sending command", ioException);
163                 // If an error occurs, close the connection
164                 close();
165             }
166
167             logger.debug("Command sent to AVR @{}: {}", getConnectionName(), command);
168         }
169
170         return isSent;
171     }
172
173     @Override
174     public boolean sendPowerQuery(int zone) {
175         return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.POWER_QUERY, zone));
176     }
177
178     @Override
179     public boolean sendVolumeQuery(int zone) {
180         return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.VOLUME_QUERY, zone));
181     }
182
183     @Override
184     public boolean sendMuteQuery(int zone) {
185         return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.MUTE_QUERY, zone));
186     }
187
188     @Override
189     public boolean sendInputSourceQuery(int zone) {
190         return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.INPUT_QUERY, zone));
191     }
192
193     @Override
194     public boolean sendListeningModeQuery(int zone) {
195         return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.LISTENING_MODE_QUERY, zone));
196     }
197
198     @Override
199     public boolean sendMCACCMemoryQuery() {
200         return sendCommand(RequestResponseFactory.getIpControlCommand(SimpleCommandType.MCACC_MEMORY_QUERY));
201     }
202
203     @Override
204     public boolean sendPowerCommand(Command command, int zone) throws CommandTypeNotSupportedException {
205         AvrCommand commandToSend = null;
206
207         if (command == OnOffType.ON) {
208             commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.POWER_ON, zone);
209             // Send the first Power ON command.
210             sendCommand(commandToSend);
211
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.
215             try {
216                 TimeUnit.MILLISECONDS.sleep(100);
217             } catch (InterruptedException ex) {
218                 Thread.currentThread().interrupt();
219             }
220         } else if (command == OnOffType.OFF) {
221             commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.POWER_OFF, zone);
222         } else {
223             throw new CommandTypeNotSupportedException("Command type not supported.");
224         }
225
226         return sendCommand(commandToSend);
227     }
228
229     @Override
230     public boolean sendVolumeCommand(Command command, int zone) throws CommandTypeNotSupportedException {
231         boolean commandSent = false;
232
233         // The OnOffType for volume is equal to the Mute command
234         if (command instanceof OnOffType) {
235             commandSent = sendMuteCommand(command, zone);
236         } else {
237             AvrCommand commandToSend = null;
238
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 percentCommand) {
244                 String ipControlVolume = VolumeConverter
245                         .convertFromPercentToIpControlVolume(percentCommand.doubleValue(), zone);
246                 commandToSend = RequestResponseFactory.getIpControlCommand(ParameterizedCommandType.VOLUME_SET, zone)
247                         .setParameter(ipControlVolume);
248             } else if (command instanceof DecimalType decimalCommand) {
249                 String ipControlVolume = VolumeConverter.convertFromDbToIpControlVolume(decimalCommand.doubleValue(),
250                         zone);
251                 commandToSend = RequestResponseFactory.getIpControlCommand(ParameterizedCommandType.VOLUME_SET, zone)
252                         .setParameter(ipControlVolume);
253             } else {
254                 throw new CommandTypeNotSupportedException("Command type not supported.");
255             }
256
257             commandSent = sendCommand(commandToSend);
258         }
259         return commandSent;
260     }
261
262     @Override
263     public boolean sendInputSourceCommand(Command command, int zone) throws CommandTypeNotSupportedException {
264         AvrCommand commandToSend = null;
265
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 stringCommand) {
271             String inputSourceValue = stringCommand.toString();
272             commandToSend = RequestResponseFactory.getIpControlCommand(ParameterizedCommandType.INPUT_CHANNEL_SET, zone)
273                     .setParameter(inputSourceValue);
274         } else {
275             throw new CommandTypeNotSupportedException("Command type not supported.");
276         }
277
278         return sendCommand(commandToSend);
279     }
280
281     @Override
282     public boolean sendListeningModeCommand(Command command, int zone) throws CommandTypeNotSupportedException {
283         AvrCommand commandToSend = null;
284
285         if (command == IncreaseDecreaseType.INCREASE) {
286             commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.LISTENING_MODE_CHANGE_CYCLIC,
287                     zone);
288         } else if (command instanceof StringType stringCommand) {
289             String listeningModeValue = stringCommand.toString();
290             commandToSend = RequestResponseFactory
291                     .getIpControlCommand(ParameterizedCommandType.LISTENING_MODE_SET, zone)
292                     .setParameter(listeningModeValue);
293         } else {
294             throw new CommandTypeNotSupportedException("Command type not supported.");
295         }
296
297         return sendCommand(commandToSend);
298     }
299
300     @Override
301     public boolean sendMuteCommand(Command command, int zone) throws CommandTypeNotSupportedException {
302         AvrCommand commandToSend = null;
303
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);
308         } else {
309             throw new CommandTypeNotSupportedException("Command type not supported.");
310         }
311
312         return sendCommand(commandToSend);
313     }
314
315     @Override
316     public boolean sendMCACCMemoryCommand(Command command) throws CommandTypeNotSupportedException {
317         AvrCommand commandToSend = null;
318
319         if (command == IncreaseDecreaseType.INCREASE) {
320             commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.MCACC_MEMORY_CHANGE_CYCLIC);
321         } else if (command instanceof StringType stringCommand) {
322             String MCACCMemoryValue = stringCommand.toString();
323             commandToSend = RequestResponseFactory.getIpControlCommand(ParameterizedCommandType.MCACC_MEMORY_SET)
324                     .setParameter(MCACCMemoryValue);
325         } else {
326             throw new CommandTypeNotSupportedException("Command type not supported.");
327         }
328
329         return sendCommand(commandToSend);
330     }
331
332     /**
333      * Read incoming data from the AVR and notify listeners for dataReceived and disconnection.
334      *
335      * @author Antoine Besnard
336      *
337      */
338     private class IpControlInputStreamReader extends Thread {
339
340         private BufferedReader bufferedReader;
341
342         private volatile boolean stopReader;
343
344         // This latch is used to block the stop method until the reader is really stopped.
345         private CountDownLatch stopLatch;
346
347         /**
348          * Construct a reader that read the given inputStream
349          *
350          * @param ipControlSocket
351          * @throws IOException
352          */
353         public IpControlInputStreamReader(InputStream inputStream) {
354             this.bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
355             this.stopLatch = new CountDownLatch(1);
356
357             this.setDaemon(true);
358             this.setName("IpControlInputStreamReader-" + getConnectionName());
359         }
360
361         @Override
362         public void run() {
363             try {
364                 while (!stopReader && !Thread.currentThread().isInterrupted()) {
365                     String receivedData = null;
366                     try {
367                         receivedData = bufferedReader.readLine();
368                     } catch (SocketTimeoutException e) {
369                         // Do nothing. Just happen to allow the thread to check if it has to stop.
370                     }
371
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);
378                             }
379                         }
380                     }
381                 }
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);
387                 }
388             }
389
390             // Notify the stopReader method caller that the reader is stopped.
391             this.stopLatch.countDown();
392         }
393
394         /**
395          * Stop this reader. Block until the reader is really stopped.
396          */
397         public void stopReader() {
398             this.stopReader = true;
399             try {
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.
405             }
406         }
407     }
408 }