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