]> git.basschouten.com Git - openhab-addons.git/blob
df4893ad0eb1921cd81fff8cb344204a52882661
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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 sendPowerCommand(Command command, int zone) throws CommandTypeNotSupportedException {
200         AvrCommand commandToSend = null;
201
202         if (command == OnOffType.ON) {
203             commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.POWER_ON, zone);
204             // Send the first Power ON command.
205             sendCommand(commandToSend);
206
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.
210             try {
211                 TimeUnit.MILLISECONDS.sleep(100);
212             } catch (InterruptedException ex) {
213                 Thread.currentThread().interrupt();
214             }
215         } else if (command == OnOffType.OFF) {
216             commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.POWER_OFF, zone);
217         } else {
218             throw new CommandTypeNotSupportedException("Command type not supported.");
219         }
220
221         return sendCommand(commandToSend);
222     }
223
224     @Override
225     public boolean sendVolumeCommand(Command command, int zone) throws CommandTypeNotSupportedException {
226         boolean commandSent = false;
227
228         // The OnOffType for volume is equal to the Mute command
229         if (command instanceof OnOffType) {
230             commandSent = sendMuteCommand(command, zone);
231         } else {
232             AvrCommand commandToSend = null;
233
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);
248             } else {
249                 throw new CommandTypeNotSupportedException("Command type not supported.");
250             }
251
252             commandSent = sendCommand(commandToSend);
253         }
254         return commandSent;
255     }
256
257     @Override
258     public boolean sendInputSourceCommand(Command command, int zone) throws CommandTypeNotSupportedException {
259         AvrCommand commandToSend = null;
260
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);
269         } else {
270             throw new CommandTypeNotSupportedException("Command type not supported.");
271         }
272
273         return sendCommand(commandToSend);
274     }
275
276     @Override
277     public boolean sendListeningModeCommand(Command command, int zone) throws CommandTypeNotSupportedException {
278         AvrCommand commandToSend = null;
279
280         if (command == IncreaseDecreaseType.INCREASE) {
281             commandToSend = RequestResponseFactory.getIpControlCommand(SimpleCommandType.LISTENING_MODE_CHANGE_CYCLIC,
282                     zone);
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);
288         } else {
289             throw new CommandTypeNotSupportedException("Command type not supported.");
290         }
291
292         return sendCommand(commandToSend);
293     }
294
295     @Override
296     public boolean sendMuteCommand(Command command, int zone) throws CommandTypeNotSupportedException {
297         AvrCommand commandToSend = null;
298
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);
303         } else {
304             throw new CommandTypeNotSupportedException("Command type not supported.");
305         }
306
307         return sendCommand(commandToSend);
308     }
309
310     /**
311      * Read incoming data from the AVR and notify listeners for dataReceived and disconnection.
312      *
313      * @author Antoine Besnard
314      *
315      */
316     private class IpControlInputStreamReader extends Thread {
317
318         private BufferedReader bufferedReader;
319
320         private volatile boolean stopReader;
321
322         // This latch is used to block the stop method until the reader is really stopped.
323         private CountDownLatch stopLatch;
324
325         /**
326          * Construct a reader that read the given inputStream
327          *
328          * @param ipControlSocket
329          * @throws IOException
330          */
331         public IpControlInputStreamReader(InputStream inputStream) {
332             this.bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
333             this.stopLatch = new CountDownLatch(1);
334
335             this.setDaemon(true);
336             this.setName("IpControlInputStreamReader-" + getConnectionName());
337         }
338
339         @Override
340         public void run() {
341             try {
342                 while (!stopReader && !Thread.currentThread().isInterrupted()) {
343                     String receivedData = null;
344                     try {
345                         receivedData = bufferedReader.readLine();
346                     } catch (SocketTimeoutException e) {
347                         // Do nothing. Just happen to allow the thread to check if it has to stop.
348                     }
349
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);
356                             }
357                         }
358                     }
359                 }
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);
365                 }
366             }
367
368             // Notify the stopReader method caller that the reader is stopped.
369             this.stopLatch.countDown();
370         }
371
372         /**
373          * Stop this reader. Block until the reader is really stopped.
374          */
375         public void stopReader() {
376             this.stopReader = true;
377             try {
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.
383             }
384         }
385     }
386 }