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.security.SecureRandom;
24 import java.util.Random;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.openhab.core.util.HexUtils;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
32 * Represents Satel ETHM-1 module. Implements method required to connect and
33 * communicate with that module over TCP/IP protocol. The module must have
34 * integration protocol enable in DLOADX configuration options.
36 * @author Krzysztof Goworek - Initial contribution
39 public class Ethm1Module extends SatelModule {
41 private static final ByteArrayInputStream EMPTY_INPUT_STREAM = new ByteArrayInputStream(new byte[0]);
43 private final Logger logger = LoggerFactory.getLogger(Ethm1Module.class);
45 private final String host;
46 private final int port;
47 private final String encryptionKey;
50 * Creates new instance with host, port, timeout and encryption key set to
53 * @param host host name or IP of ETHM-1 module
54 * @param port TCP port the module listens on
55 * @param timeout timeout value in milliseconds for connect/read/write operations
56 * @param encryptionKey encryption key for encrypted communication
57 * @param extPayloadSupport if <code>true</code>, the module supports extended command payload for reading
60 public Ethm1Module(String host, int port, int timeout, String encryptionKey, boolean extPayloadSupport) {
61 super(timeout, extPayloadSupport);
65 this.encryptionKey = encryptionKey;
69 protected CommunicationChannel connect() throws ConnectionFailureException {
70 logger.info("Connecting to ETHM-1 module at {}:{}", host, port);
73 Socket socket = new Socket();
74 socket.connect(new InetSocketAddress(host, port), this.getTimeout());
75 logger.info("ETHM-1 module connected successfully");
77 if (encryptionKey.isBlank()) {
78 return new TCPCommunicationChannel(socket);
80 return new EncryptedCommunicationChannel(socket, encryptionKey);
82 } catch (SocketTimeoutException e) {
83 throw new ConnectionFailureException("Connection timeout", e);
84 } catch (IOException e) {
85 throw new ConnectionFailureException("IO error occurred while connecting socket", e);
89 private class TCPCommunicationChannel implements CommunicationChannel {
91 private Socket socket;
93 public TCPCommunicationChannel(Socket socket) {
98 public InputStream getInputStream() throws IOException {
99 return this.socket.getInputStream();
103 public OutputStream getOutputStream() throws IOException {
104 return this.socket.getOutputStream();
108 public void disconnect() {
109 logger.info("Closing connection to ETHM-1 module");
112 } catch (IOException e) {
113 logger.error("IO error occurred during closing socket", e);
118 private class EncryptedCommunicationChannel extends TCPCommunicationChannel {
120 private EncryptionHelper aesHelper;
124 private int rollingCounter;
125 private InputStream inputStream;
126 private OutputStream outputStream;
128 public EncryptedCommunicationChannel(final Socket socket, String encryptionKey) throws IOException {
132 this.aesHelper = new EncryptionHelper(encryptionKey);
133 } catch (Exception e) {
134 throw new IOException("General encryption failure", e);
136 this.rand = new SecureRandom();
139 this.rollingCounter = 0;
141 this.inputStream = new InputStream() {
142 private ByteArrayInputStream inputBuffer = EMPTY_INPUT_STREAM;
145 public int read() throws IOException {
146 if (inputBuffer.available() == 0) {
147 // read message and decrypt it
148 byte[] data = readMessage(socket.getInputStream());
150 inputBuffer = new ByteArrayInputStream(data, 6, data.length - 6);
152 return inputBuffer.read();
156 this.outputStream = new OutputStream() {
157 private ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(256);
160 public void write(int b) throws IOException {
161 outputBuffer.write(b);
165 public void flush() throws IOException {
166 writeMessage(outputBuffer.toByteArray(), socket.getOutputStream());
167 outputBuffer.reset();
173 public InputStream getInputStream() throws IOException {
174 return this.inputStream;
178 public OutputStream getOutputStream() throws IOException {
179 return this.outputStream;
182 private synchronized byte[] readMessage(InputStream is) throws IOException {
183 logger.trace("Receiving data from ETHM-1");
184 // read number of bytes
185 int bytesCount = is.read();
186 logger.trace("Read count of bytes: {}", bytesCount);
187 if (bytesCount == -1) {
188 throw new IOException("End of input stream reached");
190 byte[] data = new byte[bytesCount];
191 // read encrypted data
192 int bytesRead = is.read(data);
193 if (bytesCount != bytesRead) {
194 throw new IOException(
195 String.format("Too few bytes read. Read: %d, expected: %d", bytesRead, bytesCount));
198 if (logger.isTraceEnabled()) {
199 logger.trace("Decrypting data: {}", HexUtils.bytesToHex(data));
203 this.aesHelper.decrypt(data);
204 } catch (Exception e) {
205 throw new IOException("Decryption exception", e);
208 if (logger.isDebugEnabled()) {
209 logger.debug("Decrypted data: {}", HexUtils.bytesToHex(data));
214 if (this.idS != data[5]) {
215 throw new IOException(String.format("Invalid 'idS' value. Got: %d, expected: %d", data[5], this.idS));
221 private synchronized void writeMessage(byte[] message, OutputStream os) throws IOException {
222 // prepare data for encryption
223 int bytesCount = 6 + message.length;
224 if (bytesCount < 16) {
227 byte[] data = new byte[bytesCount];
228 int randomValue = this.rand.nextInt();
229 data[0] = (byte) (randomValue >> 8);
230 data[1] = (byte) (randomValue & 0xff);
231 data[2] = (byte) (this.rollingCounter >> 8);
232 data[3] = (byte) (this.rollingCounter & 0xff);
233 data[4] = this.idS = (byte) this.rand.nextInt();
235 ++this.rollingCounter;
236 System.arraycopy(message, 0, data, 6, message.length);
239 if (logger.isDebugEnabled()) {
240 logger.debug("Encrypting data: {}", HexUtils.bytesToHex(data));
244 this.aesHelper.encrypt(data);
245 } catch (Exception e) {
246 throw new IOException("Encryption exception", e);
249 if (logger.isTraceEnabled()) {
250 logger.trace("Encrypted data: {}", HexUtils.bytesToHex(data));
253 // write encrypted data to output stream
254 os.write(bytesCount);