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