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.qolsysiq.internal.client;
15 import java.io.BufferedReader;
16 import java.io.BufferedWriter;
17 import java.io.IOException;
18 import java.io.InputStreamReader;
19 import java.io.OutputStreamWriter;
20 import java.lang.reflect.Type;
21 import java.security.KeyManagementException;
22 import java.security.NoSuchAlgorithmException;
23 import java.security.cert.X509Certificate;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.List;
27 import java.util.concurrent.ScheduledExecutorService;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
31 import javax.net.ssl.SSLContext;
32 import javax.net.ssl.SSLSocket;
33 import javax.net.ssl.SSLSocketFactory;
34 import javax.net.ssl.TrustManager;
35 import javax.net.ssl.X509TrustManager;
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.openhab.binding.qolsysiq.internal.client.dto.action.Action;
40 import org.openhab.binding.qolsysiq.internal.client.dto.event.AlarmEvent;
41 import org.openhab.binding.qolsysiq.internal.client.dto.event.ArmingEvent;
42 import org.openhab.binding.qolsysiq.internal.client.dto.event.ErrorEvent;
43 import org.openhab.binding.qolsysiq.internal.client.dto.event.Event;
44 import org.openhab.binding.qolsysiq.internal.client.dto.event.EventType;
45 import org.openhab.binding.qolsysiq.internal.client.dto.event.InfoEventType;
46 import org.openhab.binding.qolsysiq.internal.client.dto.event.SecureArmInfoEvent;
47 import org.openhab.binding.qolsysiq.internal.client.dto.event.SummaryInfoEvent;
48 import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneActiveEvent;
49 import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneAddEvent;
50 import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneEventType;
51 import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneUpdateEvent;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
55 import com.google.gson.FieldNamingPolicy;
56 import com.google.gson.Gson;
57 import com.google.gson.GsonBuilder;
58 import com.google.gson.JsonDeserializationContext;
59 import com.google.gson.JsonDeserializer;
60 import com.google.gson.JsonElement;
61 import com.google.gson.JsonObject;
62 import com.google.gson.JsonParseException;
63 import com.google.gson.JsonSyntaxException;
66 * A client that can communicate with a Qolsys IQ Panel
68 * @author Dan Cunningham - Initial contribution
71 public class QolsysiqClient {
72 private static final String MESSAGE_ACK = "ACK";
73 private final Logger logger = LoggerFactory.getLogger(QolsysiqClient.class);
74 private final Gson gson = new GsonBuilder().registerTypeAdapter(Event.class, new EventDeserializer())
75 .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
76 private List<QolsysIQClientListener> listeners = Collections.synchronizedList(new ArrayList<>());
77 private @Nullable SSLSocket socket;
78 private @Nullable BufferedReader reader;
79 private @Nullable BufferedWriter writer;
80 private @Nullable Thread readerThread;
81 private @Nullable ScheduledFuture<?> heartBeatFuture;
82 private ScheduledExecutorService scheduler;
83 private Object writeLock = new Object();
84 private long lastResponseTime;
85 private boolean hasACK = false;
86 private boolean connected;
89 private int heartbeatSeconds;
90 private String threadName;
91 private SSLSocketFactory sslsocketfactory;
94 * Creates a new QolsysiqClient
98 * @param heartbeatSeconds
99 * @param scheduler for the heart beat task
102 public QolsysiqClient(String host, int port, int heartbeatSeconds, ScheduledExecutorService scheduler,
103 String threadName) throws IOException {
106 this.heartbeatSeconds = heartbeatSeconds;
107 this.scheduler = scheduler;
108 this.threadName = threadName;
111 SSLContext sslContext = SSLContext.getInstance("TLS");
112 sslContext.init(null, acceptAlltrustManagers(), null);
113 sslsocketfactory = sslContext.getSocketFactory();
114 } catch (KeyManagementException | NoSuchAlgorithmException e) {
115 throw new IOException(e);
120 * Connects to the panel
122 * @throws IOException
124 public synchronized void connect() throws IOException {
125 logger.debug("connect");
127 logger.debug("connect: already connected, ignoring");
131 SSLSocket socket = (SSLSocket) sslsocketfactory.createSocket(host, port);
132 socket.startHandshake();
133 writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
134 reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
135 this.socket = socket;
137 Thread readerThread = new Thread(this::readEvents, threadName);
138 readerThread.setDaemon(true);
139 readerThread.start();
140 this.readerThread = readerThread;
143 // send an initial message to confirm a connection and record a response time
145 } catch (IOException e) {
146 // clean up before bubbling up exception
150 heartBeatFuture = scheduler.scheduleWithFixedDelay(() -> {
153 if (System.currentTimeMillis() - lastResponseTime > (heartbeatSeconds + 5) * 1000) {
154 throw new IOException("No responses received");
157 } catch (IOException e) {
158 logger.debug("Problem sending heartbeat", e);
159 disconnectAndNotify(e);
162 }, heartbeatSeconds, heartbeatSeconds, TimeUnit.SECONDS);
166 * Disconnects from the panel
168 public void disconnect() {
171 ScheduledFuture<?> heartbeatFuture = this.heartBeatFuture;
172 if (heartbeatFuture != null) {
173 heartbeatFuture.cancel(true);
176 Thread readerThread = this.readerThread;
177 if (readerThread != null && readerThread.isAlive()) {
178 readerThread.interrupt();
181 SSLSocket socket = this.socket;
182 if (socket != null) {
185 } catch (IOException e) {
186 logger.debug("Error closing SSL socket: {}", e.getMessage());
190 BufferedReader reader = this.reader;
191 if (reader != null) {
194 } catch (IOException e) {
195 logger.debug("Error closing reader: {}", e.getMessage());
199 BufferedWriter writer = this.writer;
200 if (writer != null) {
203 } catch (IOException e) {
204 logger.debug("Error closing writer: {}", e.getMessage());
211 * Sends an Action message to the panel
214 * @throws IOException
216 public void sendAction(Action action) throws IOException {
217 logger.debug("sendAction {}", action.type);
218 writeMessage(gson.toJson(action));
222 * Adds a QolsysIQClientListener
226 public void addListener(QolsysIQClientListener listener) {
227 synchronized (listeners) {
228 listeners.add(listener);
233 * Removes a QolsysIQClientListener
237 public void removeListener(QolsysIQClientListener listener) {
238 synchronized (listeners) {
239 listeners.remove(listener);
243 private synchronized void writeMessage(String message) throws IOException {
245 logger.debug("writeMessage: not connected, ignoring {}", message);
248 synchronized (writeLock) {
250 logger.trace("writeMessage: {}", message);
251 BufferedWriter writer = this.writer;
252 if (writer != null) {
253 writer.write(message);
257 writeLock.wait(5000);
258 } catch (InterruptedException e) {
259 logger.debug("write lock interupted");
262 logger.trace("writeMessage: no ACK for {}", message);
263 throw new IOException("No response to message: " + message);
269 private void readEvents() {
271 BufferedReader reader = this.reader;
273 while (connected && reader != null && (message = reader.readLine()) != null) {
274 logger.trace("Message: {}", message);
275 lastResponseTime = System.currentTimeMillis();
276 if (MESSAGE_ACK.equals(message)) {
277 synchronized (writeLock) {
284 Event event = gson.fromJson(message, Event.class);
286 logger.debug("Could not deserialize message: {}", message);
289 synchronized (listeners) {
290 if (event instanceof AlarmEvent) {
291 listeners.forEach(listener -> listener.alarmEvent((AlarmEvent) event));
292 } else if (event instanceof ArmingEvent) {
293 listeners.forEach(listener -> listener.armingEvent((ArmingEvent) event));
294 } else if (event instanceof ErrorEvent) {
295 listeners.forEach(listener -> listener.errorEvent((ErrorEvent) event));
296 } else if (event instanceof SecureArmInfoEvent) {
297 listeners.forEach(listener -> listener.secureArmInfoEvent((SecureArmInfoEvent) event));
298 } else if (event instanceof SummaryInfoEvent) {
299 listeners.forEach(listener -> listener.summaryInfoEvent((SummaryInfoEvent) event));
300 } else if (event instanceof ZoneActiveEvent) {
301 listeners.forEach(listener -> listener.zoneActiveEvent((ZoneActiveEvent) event));
302 } else if (event instanceof ZoneUpdateEvent) {
303 listeners.forEach(listener -> listener.zoneUpdateEvent((ZoneUpdateEvent) event));
304 } else if (event instanceof ZoneAddEvent) {
305 listeners.forEach(listener -> listener.zoneAddEvent((ZoneAddEvent) event));
308 } catch (JsonSyntaxException e) {
309 logger.debug("Could not parse messge", e);
313 throw new IOException("socket disconencted");
315 } catch (IOException e) {
316 disconnectAndNotify(e);
320 private void disconnectAndNotify(Exception e) {
323 synchronized (listeners) {
324 listeners.forEach(listener -> listener.disconnected(e));
329 private TrustManager[] acceptAlltrustManagers() {
330 return new TrustManager[] { new X509TrustManager() {
332 public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
336 public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
340 public X509Certificate @Nullable [] getAcceptedIssuers() {
346 class EventDeserializer implements JsonDeserializer<Event> {
348 public @Nullable Event deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
349 throws JsonParseException {
350 JsonObject jsonObject = json.getAsJsonObject();
351 JsonElement event = jsonObject.get("event");
353 switch (EventType.valueOf(event.getAsString())) {
355 return context.deserialize(jsonObject, AlarmEvent.class);
357 return context.deserialize(jsonObject, ArmingEvent.class);
359 return context.deserialize(jsonObject, ErrorEvent.class);
361 JsonElement infoType = jsonObject.get("info_type");
362 if (infoType != null) {
363 switch (InfoEventType.valueOf(infoType.getAsString())) {
365 return context.deserialize(jsonObject, SecureArmInfoEvent.class);
367 return context.deserialize(jsonObject, SummaryInfoEvent.class);
372 JsonElement zoneEventType = jsonObject.get("zone_event_type");
373 if (zoneEventType != null) {
374 switch (ZoneEventType.valueOf(zoneEventType.getAsString())) {
376 return context.deserialize(jsonObject, ZoneActiveEvent.class);
378 return context.deserialize(jsonObject, ZoneUpdateEvent.class);
380 return context.deserialize(jsonObject, ZoneAddEvent.class);