2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.satel.internal.protocol;
15 import java.io.ByteArrayInputStream;
16 import java.io.ByteArrayOutputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.OutputStream;
20 import java.net.InetSocketAddress;
21 import java.net.Socket;
22 import java.net.SocketTimeoutException;
23 import java.util.Random;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.openhab.core.util.HexUtils;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
31 * Represents Satel ETHM-1 module. Implements method required to connect and
32 * communicate with that module over TCP/IP protocol. The module must have
33 * integration protocol enable in DLOADX configuration options.
35 * @author Krzysztof Goworek - Initial contribution
38 public class Ethm1Module extends SatelModule {
40 private static final ByteArrayInputStream EMPTY_INPUT_STREAM = new ByteArrayInputStream(new byte[0]);
42 private final Logger logger = LoggerFactory.getLogger(Ethm1Module.class);
44 private final String host;
45 private final int port;
46 private final String encryptionKey;
49 * Creates new instance with host, port, timeout and encryption key set to
52 * @param host host name or IP of ETHM-1 module
53 * @param port TCP port the module listens on
54 * @param timeout timeout value in milliseconds for connect/read/write operations
55 * @param encryptionKey encryption key for encrypted communication
56 * @param extPayloadSupport if <code>true</code>, the module supports extended command payload for reading
59 public Ethm1Module(String host, int port, int timeout, String encryptionKey, boolean extPayloadSupport) {
60 super(timeout, extPayloadSupport);
64 this.encryptionKey = encryptionKey;
68 protected CommunicationChannel connect() throws ConnectionFailureException {
69 logger.info("Connecting to ETHM-1 module at {}:{}", host, port);
72 Socket socket = new Socket();
73 socket.connect(new InetSocketAddress(host, port), this.getTimeout());
74 logger.info("ETHM-1 module connected successfully");
76 if (encryptionKey.isBlank()) {
77 return new TCPCommunicationChannel(socket);
79 return new EncryptedCommunicationChannel(socket, encryptionKey);
81 } catch (SocketTimeoutException e) {
82 throw new ConnectionFailureException("Connection timeout", e);
83 } catch (IOException e) {
84 throw new ConnectionFailureException("IO error occurred while connecting socket", e);
88 private class TCPCommunicationChannel implements CommunicationChannel {
90 private Socket socket;
92 public TCPCommunicationChannel(Socket socket) {
97 public InputStream getInputStream() throws IOException {
98 return this.socket.getInputStream();
102 public OutputStream getOutputStream() throws IOException {
103 return this.socket.getOutputStream();
107 public void disconnect() {
108 logger.info("Closing connection to ETHM-1 module");
111 } catch (IOException e) {
112 logger.error("IO error occurred during closing socket", e);
117 private class EncryptedCommunicationChannel extends TCPCommunicationChannel {
119 private EncryptionHelper aesHelper;
123 private int rollingCounter;
124 private InputStream inputStream;
125 private OutputStream outputStream;
127 public EncryptedCommunicationChannel(final Socket socket, String encryptionKey) throws IOException {
131 this.aesHelper = new EncryptionHelper(encryptionKey);
132 } catch (Exception e) {
133 throw new IOException("General encryption failure", e);
135 this.rand = new Random();
138 this.rollingCounter = 0;
140 this.inputStream = new InputStream() {
141 private ByteArrayInputStream inputBuffer = EMPTY_INPUT_STREAM;
144 public int read() throws IOException {
145 if (inputBuffer.available() == 0) {
146 // read message and decrypt it
147 byte[] data = readMessage(socket.getInputStream());
149 inputBuffer = new ByteArrayInputStream(data, 6, data.length - 6);
151 return inputBuffer.read();
155 this.outputStream = new OutputStream() {
156 private ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(256);
159 public void write(int b) throws IOException {
160 outputBuffer.write(b);
164 public void flush() throws IOException {
165 writeMessage(outputBuffer.toByteArray(), socket.getOutputStream());
166 outputBuffer.reset();
172 public InputStream getInputStream() throws IOException {
173 return this.inputStream;
177 public OutputStream getOutputStream() throws IOException {
178 return this.outputStream;
181 private synchronized byte[] readMessage(InputStream is) throws IOException {
182 logger.trace("Receiving data from ETHM-1");
183 // read number of bytes
184 int bytesCount = is.read();
185 logger.trace("Read count of bytes: {}", bytesCount);
186 if (bytesCount == -1) {
187 throw new IOException("End of input stream reached");
189 byte[] data = new byte[bytesCount];
190 // read encrypted data
191 int bytesRead = is.read(data);
192 if (bytesCount != bytesRead) {
193 throw new IOException(
194 String.format("Too few bytes read. Read: %d, expected: %d", bytesRead, bytesCount));
197 if (logger.isTraceEnabled()) {
198 logger.trace("Decrypting data: {}", HexUtils.bytesToHex(data));
202 this.aesHelper.decrypt(data);
203 } catch (Exception e) {
204 throw new IOException("Decryption exception", e);
207 if (logger.isDebugEnabled()) {
208 logger.debug("Decrypted data: {}", HexUtils.bytesToHex(data));
213 if (this.idS != data[5]) {
214 throw new IOException(String.format("Invalid 'idS' value. Got: %d, expected: %d", data[5], this.idS));
220 private synchronized void writeMessage(byte[] message, OutputStream os) throws IOException {
221 // prepare data for encryption
222 int bytesCount = 6 + message.length;
223 if (bytesCount < 16) {
226 byte[] data = new byte[bytesCount];
227 int randomValue = this.rand.nextInt();
228 data[0] = (byte) (randomValue >> 8);
229 data[1] = (byte) (randomValue & 0xff);
230 data[2] = (byte) (this.rollingCounter >> 8);
231 data[3] = (byte) (this.rollingCounter & 0xff);
232 data[4] = this.idS = (byte) this.rand.nextInt();
234 ++this.rollingCounter;
235 System.arraycopy(message, 0, data, 6, message.length);
238 if (logger.isDebugEnabled()) {
239 logger.debug("Encrypting data: {}", HexUtils.bytesToHex(data));
243 this.aesHelper.encrypt(data);
244 } catch (Exception e) {
245 throw new IOException("Encryption exception", e);
248 if (logger.isTraceEnabled()) {
249 logger.trace("Encrypted data: {}", HexUtils.bytesToHex(data));
252 // write encrypted data to output stream
253 os.write(bytesCount);