]> git.basschouten.com Git - openhab-addons.git/blob
21d7a394177917ac489594776cc0279f5bc5025a
[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.sonyprojector.internal.communication.sdcp;
14
15 import java.io.DataInputStream;
16 import java.io.DataOutputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.net.Socket;
20 import java.net.SocketTimeoutException;
21 import java.nio.charset.StandardCharsets;
22 import java.util.Arrays;
23 import java.util.concurrent.TimeUnit;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.sonyprojector.internal.SonyProjectorException;
28 import org.openhab.binding.sonyprojector.internal.SonyProjectorModel;
29 import org.openhab.binding.sonyprojector.internal.communication.SonyProjectorConnector;
30 import org.openhab.binding.sonyprojector.internal.communication.SonyProjectorItem;
31 import org.openhab.core.util.HexUtils;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * Class for communicating with Sony Projectors through an IP connection
37  * using Pj Talk service (SDCP protocol)
38  *
39  * @author Markus Wehrle - Initial contribution
40  * @author Laurent Garnier - Refactoring to consider SonyProjectorConnector and add a full check of responses
41  */
42 @NonNullByDefault
43 public class SonyProjectorSdcpConnector extends SonyProjectorConnector {
44
45     private final Logger logger = LoggerFactory.getLogger(SonyProjectorSdcpConnector.class);
46
47     private static final int DEFAULT_PORT = 53484;
48     private static final String DEFAULT_COMMUNITY = "SONY";
49     private static final long READ_TIMEOUT_MS = TimeUnit.MILLISECONDS.toMillis(3500);
50     private static final int MSG_MIN_SIZE = 10;
51     private static final int MSG_MAX_SIZE = 34;
52
53     protected static final byte[] HEADER = new byte[] { 0x02, 0x0A };
54     private static final byte SET = 0x00;
55     private static final byte GET = 0x01;
56     protected static final byte OK = 0x01;
57
58     private String address;
59     private int port;
60     private String community;
61
62     private @Nullable Socket clientSocket;
63
64     /**
65      * Constructor
66      *
67      * @param address the IP address of the projector
68      * @param port the TCP port to be used
69      * @param community the community name of the equipment
70      * @param model the projector model in use
71      */
72     public SonyProjectorSdcpConnector(String address, @Nullable Integer port, @Nullable String community,
73             SonyProjectorModel model) {
74         this(address, port, community, model, false);
75     }
76
77     /**
78      * Constructor
79      *
80      * @param address the IP address of the projector
81      * @param port the TCP port to be used
82      * @param community the community name of the equipment
83      * @param model the projector model in use
84      * @param simu whether the communication is simulated or real
85      */
86     protected SonyProjectorSdcpConnector(String address, @Nullable Integer port, @Nullable String community,
87             SonyProjectorModel model, boolean simu) {
88         super(model, false);
89
90         this.address = address;
91
92         // init port
93         if (port != null && port > 0) {
94             this.port = port;
95         } else {
96             this.port = DEFAULT_PORT;
97         }
98
99         // init community
100         if (community != null && !community.isEmpty() && community.length() == 4) {
101             this.community = community;
102         } else {
103             this.community = DEFAULT_COMMUNITY;
104         }
105     }
106
107     /**
108      * Get the community name of the equipment
109      *
110      * @return the community name of the equipment
111      */
112     public String getCommunity() {
113         return community;
114     }
115
116     @Override
117     public synchronized void open() throws SonyProjectorException {
118         if (!connected) {
119             logger.debug("Opening SDCP connection IP {} port {}", this.address, this.port);
120             try {
121                 Socket clientSocket = new Socket(this.address, this.port);
122                 clientSocket.setSoTimeout(200);
123
124                 dataOut = new DataOutputStream(clientSocket.getOutputStream());
125                 dataIn = new DataInputStream(clientSocket.getInputStream());
126
127                 this.clientSocket = clientSocket;
128
129                 connected = true;
130
131                 logger.debug("SDCP connection opened");
132             } catch (IOException | SecurityException | IllegalArgumentException e) {
133                 logger.debug("Opening SDCP connection failed: {}", e.getMessage());
134                 throw new SonyProjectorException("Opening SDCP connection failed: " + e.getMessage());
135             }
136         }
137     }
138
139     @Override
140     public synchronized void close() {
141         if (connected) {
142             logger.debug("closing SDCP connection");
143             super.close();
144             Socket clientSocket = this.clientSocket;
145             if (clientSocket != null) {
146                 try {
147                     clientSocket.close();
148                 } catch (IOException e) {
149                 }
150                 this.clientSocket = null;
151             }
152             connected = false;
153         }
154     }
155
156     @Override
157     protected byte[] buildMessage(SonyProjectorItem item, boolean getCommand, byte[] data) {
158         byte[] communityData = community.getBytes();
159         byte[] message = new byte[10 + data.length];
160         message[0] = HEADER[0];
161         message[1] = HEADER[1];
162         message[2] = communityData[0];
163         message[3] = communityData[1];
164         message[4] = communityData[2];
165         message[5] = communityData[3];
166         message[6] = getCommand ? GET : SET;
167         message[7] = item.getCode()[0];
168         message[8] = item.getCode()[1];
169         message[9] = getCommand ? 0 : (byte) data.length;
170         if (!getCommand) {
171             System.arraycopy(data, 0, message, 10, data.length);
172         }
173         return message;
174     }
175
176     /**
177      * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
178      * actually read is returned as an integer.
179      * In case of socket timeout, the returned value is 0.
180      *
181      * @param dataBuffer the buffer into which the data is read.
182      * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
183      *         stream has been reached.
184      * @throws SonyProjectorException - If the input stream is null, if the first byte cannot be read for any reason
185      *             other than the end of the file, if the input stream has been closed, or if some other I/O error
186      *             occurs.
187      */
188     @Override
189     protected int readInput(byte[] dataBuffer) throws SonyProjectorException {
190         InputStream dataIn = this.dataIn;
191         if (dataIn == null) {
192             throw new SonyProjectorException("readInput failed: input stream is null");
193         }
194         try {
195             return dataIn.read(dataBuffer);
196         } catch (SocketTimeoutException e) {
197             return 0;
198         } catch (IOException e) {
199             logger.debug("readInput failed: {}", e.getMessage());
200             throw new SonyProjectorException("readInput failed: " + e.getMessage());
201         }
202     }
203
204     @Override
205     protected synchronized byte[] readResponse() throws SonyProjectorException {
206         logger.debug("readResponse (timeout = {} ms)...", READ_TIMEOUT_MS);
207         byte[] message = new byte[MSG_MAX_SIZE];
208         boolean timeout = false;
209         byte[] dataBuffer = new byte[MSG_MAX_SIZE];
210         int count = 0;
211         long startTimeRead = System.currentTimeMillis();
212         while ((count < MSG_MIN_SIZE) && !timeout) {
213             logger.trace("readResponse readInput...");
214             int len = readInput(dataBuffer);
215             logger.trace("readResponse readInput {} => {}", len, HexUtils.bytesToHex(dataBuffer));
216             if (len > 0) {
217                 int oldCount = count;
218                 count = ((oldCount + len) > MSG_MAX_SIZE) ? MSG_MAX_SIZE : (oldCount + len);
219                 System.arraycopy(dataBuffer, 0, message, oldCount, count - oldCount);
220             }
221             timeout = (System.currentTimeMillis() - startTimeRead) > READ_TIMEOUT_MS;
222         }
223         if ((count < MSG_MIN_SIZE) && timeout) {
224             logger.debug("readResponse timeout: only {} bytes read after {} ms", count, READ_TIMEOUT_MS);
225             throw new SonyProjectorException("readResponse failed: timeout");
226         }
227         logger.debug("readResponse: {}", HexUtils.bytesToHex(message));
228         if (count < MSG_MIN_SIZE) {
229             logger.debug("readResponse: unexpected response data length: {}", count);
230             throw new SonyProjectorException("Unexpected response data length");
231         }
232         return message;
233     }
234
235     @Override
236     protected void validateResponse(byte[] responseMessage, SonyProjectorItem item) throws SonyProjectorException {
237         // Check response size
238         if (responseMessage.length < MSG_MIN_SIZE) {
239             logger.debug("Unexpected response data length: {}", responseMessage.length);
240             throw new SonyProjectorException("Unexpected response data length");
241         }
242
243         // Header should be a sony projector header
244         byte[] headerMsg = Arrays.copyOf(responseMessage, HEADER.length);
245         if (!Arrays.equals(headerMsg, HEADER)) {
246             logger.debug("Unexpected HEADER in response: {} rather than {}", HexUtils.bytesToHex(headerMsg),
247                     HexUtils.bytesToHex(HEADER));
248             throw new SonyProjectorException("Unexpected HEADER in response");
249         }
250
251         // Community should be the same as used for sending
252         byte[] communityResponseMsg = Arrays.copyOfRange(responseMessage, 2, 6);
253         if (!Arrays.equals(communityResponseMsg, community.getBytes())) {
254             logger.debug("Unexpected community in response: {} rather than {}",
255                     HexUtils.bytesToHex(communityResponseMsg), HexUtils.bytesToHex(community.getBytes()));
256             throw new SonyProjectorException("Unexpected community in response");
257         }
258
259         // Item number should be the same as used for sending
260         byte[] itemResponseMsg = Arrays.copyOfRange(responseMessage, 7, 9);
261         if (!Arrays.equals(itemResponseMsg, item.getCode())) {
262             logger.debug("Unexpected item number in response: {} rather than {}", HexUtils.bytesToHex(itemResponseMsg),
263                     HexUtils.bytesToHex(item.getCode()));
264             throw new SonyProjectorException("Unexpected item number in response");
265         }
266
267         // Check response size
268         int dataLength = responseMessage[9] & 0x000000FF;
269         if (responseMessage.length < (10 + dataLength)) {
270             logger.debug("Unexpected response data length: {}", dataLength);
271             throw new SonyProjectorException("Unexpected response data length");
272         }
273
274         // byte 7 is expected to be 1, which indicates that the request was successful
275         if (responseMessage[6] != OK) {
276             String msg = "KO";
277             if (dataLength == 12) {
278                 byte[] errorCode = Arrays.copyOfRange(responseMessage, 10, 12);
279                 try {
280                     SonyProjectorSdcpError error = SonyProjectorSdcpError.getFromDataCode(errorCode);
281                     msg = error.getMessage();
282                 } catch (SonyProjectorException e) {
283                 }
284             }
285             logger.debug("{} received in response", msg);
286             throw new SonyProjectorException(msg + " received in response");
287         }
288     }
289
290     @Override
291     protected byte[] getResponseData(byte[] responseMessage) {
292         // Data length is in 10th byte of message
293         int dataLength = responseMessage[9] & 0x000000FF;
294         if (dataLength > 0) {
295             return Arrays.copyOfRange(responseMessage, 10, 10 + dataLength);
296         } else {
297             return new byte[] { (byte) 0xFF };
298         }
299     }
300
301     /**
302      * Request the model name
303      *
304      * @return the model name
305      *
306      * @throws SonyProjectorException - In case of any problem
307      */
308     public String getModelName() throws SonyProjectorException {
309         return new String(getSetting(SonyProjectorItem.MODEL_NAME), StandardCharsets.UTF_8);
310     }
311 }