]> git.basschouten.com Git - openhab-addons.git/blob
81f6b9ffaefa9ddcbf3933ae1fb3590a2bc76c80
[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.qolsysiq.internal.client;
14
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;
30
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;
36
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;
54
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;
64
65 /**
66  * A client that can communicate with a Qolsys IQ Panel
67  *
68  * @author Dan Cunningham - Initial contribution
69  */
70 @NonNullByDefault
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;
87     private String host;
88     private int port;
89     private int heartbeatSeconds;
90     private String threadName;
91     private SSLSocketFactory sslsocketfactory;
92
93     /**
94      * Creates a new QolsysiqClient
95      *
96      * @param host
97      * @param port
98      * @param heartbeatSeconds
99      * @param scheduler for the heart beat task
100      * @param threadName
101      */
102     public QolsysiqClient(String host, int port, int heartbeatSeconds, ScheduledExecutorService scheduler,
103             String threadName) throws IOException {
104         this.host = host;
105         this.port = port;
106         this.heartbeatSeconds = heartbeatSeconds;
107         this.scheduler = scheduler;
108         this.threadName = threadName;
109
110         try {
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);
116         }
117     }
118
119     /**
120      * Connects to the panel
121      *
122      * @throws IOException
123      */
124     public synchronized void connect() throws IOException {
125         logger.debug("connect");
126         if (connected) {
127             logger.debug("connect: already connected, ignoring");
128             return;
129         }
130
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;
136
137         Thread readerThread = new Thread(this::readEvents, threadName);
138         readerThread.setDaemon(true);
139         readerThread.start();
140         this.readerThread = readerThread;
141         connected = true;
142         try {
143             // send an initial message to confirm a connection and record a response time
144             writeMessage("");
145         } catch (IOException e) {
146             // clean up before bubbling up exception
147             disconnect();
148             throw e;
149         }
150         heartBeatFuture = scheduler.scheduleWithFixedDelay(() -> {
151             if (connected) {
152                 try {
153                     if (System.currentTimeMillis() - lastResponseTime > (heartbeatSeconds + 5) * 1000) {
154                         throw new IOException("No responses received");
155                     }
156                     writeMessage("");
157                 } catch (IOException e) {
158                     logger.debug("Problem sending heartbeat", e);
159                     disconnectAndNotify(e);
160                 }
161             }
162         }, heartbeatSeconds, heartbeatSeconds, TimeUnit.SECONDS);
163     }
164
165     /**
166      * Disconnects from the panel
167      */
168     public void disconnect() {
169         connected = false;
170
171         ScheduledFuture<?> heartbeatFuture = this.heartBeatFuture;
172         if (heartbeatFuture != null) {
173             heartbeatFuture.cancel(true);
174         }
175
176         Thread readerThread = this.readerThread;
177         if (readerThread != null && readerThread.isAlive()) {
178             readerThread.interrupt();
179         }
180
181         SSLSocket socket = this.socket;
182         if (socket != null) {
183             try {
184                 socket.close();
185             } catch (IOException e) {
186                 logger.debug("Error closing SSL socket: {}", e.getMessage());
187             }
188             this.socket = null;
189         }
190         BufferedReader reader = this.reader;
191         if (reader != null) {
192             try {
193                 reader.close();
194             } catch (IOException e) {
195                 logger.debug("Error closing reader: {}", e.getMessage());
196             }
197             this.reader = null;
198         }
199         BufferedWriter writer = this.writer;
200         if (writer != null) {
201             try {
202                 writer.close();
203             } catch (IOException e) {
204                 logger.debug("Error closing writer: {}", e.getMessage());
205             }
206             this.writer = null;
207         }
208     }
209
210     /**
211      * Sends an Action message to the panel
212      *
213      * @param action
214      * @throws IOException
215      */
216     public void sendAction(Action action) throws IOException {
217         logger.debug("sendAction {}", action.type);
218         writeMessage(gson.toJson(action));
219     }
220
221     /**
222      * Adds a QolsysIQClientListener
223      *
224      * @param listener
225      */
226     public void addListener(QolsysIQClientListener listener) {
227         synchronized (listeners) {
228             listeners.add(listener);
229         }
230     }
231
232     /**
233      * Removes a QolsysIQClientListener
234      *
235      * @param listener
236      */
237     public void removeListener(QolsysIQClientListener listener) {
238         synchronized (listeners) {
239             listeners.remove(listener);
240         }
241     }
242
243     private synchronized void writeMessage(String message) throws IOException {
244         if (!connected) {
245             logger.debug("writeMessage: not connected, ignoring {}", message);
246             return;
247         }
248         synchronized (writeLock) {
249             hasACK = false;
250             logger.trace("writeMessage: {}", message);
251             BufferedWriter writer = this.writer;
252             if (writer != null) {
253                 writer.write(message);
254                 writer.newLine();
255                 writer.flush();
256                 try {
257                     writeLock.wait(5000);
258                 } catch (InterruptedException e) {
259                     logger.debug("write lock interupted");
260                 }
261                 if (!hasACK) {
262                     logger.trace("writeMessage: no ACK for {}", message);
263                     throw new IOException("No response to message: " + message);
264                 }
265             }
266         }
267     }
268
269     private void readEvents() {
270         String message;
271         BufferedReader reader = this.reader;
272         try {
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) {
278                         hasACK = true;
279                         writeLock.notify();
280                     }
281                     continue;
282                 }
283                 try {
284                     Event event = gson.fromJson(message, Event.class);
285                     if (event == null) {
286                         logger.debug("Could not deserialize message: {}", message);
287                         continue;
288                     }
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));
306                         }
307                     }
308                 } catch (JsonSyntaxException e) {
309                     logger.debug("Could not parse messge", e);
310                 }
311             }
312             if (connected) {
313                 throw new IOException("socket disconencted");
314             }
315         } catch (IOException e) {
316             disconnectAndNotify(e);
317         }
318     }
319
320     private void disconnectAndNotify(Exception e) {
321         if (connected) {
322             disconnect();
323             synchronized (listeners) {
324                 listeners.forEach(listener -> listener.disconnected(e));
325             }
326         }
327     }
328
329     private TrustManager[] acceptAlltrustManagers() {
330         return new TrustManager[] { new X509TrustManager() {
331             @Override
332             public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
333             }
334
335             @Override
336             public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
337             }
338
339             @Override
340             public X509Certificate @Nullable [] getAcceptedIssuers() {
341                 return null;
342             }
343         } };
344     }
345
346     class EventDeserializer implements JsonDeserializer<Event> {
347         @Override
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");
352             if (event != null) {
353                 switch (EventType.valueOf(event.getAsString())) {
354                     case ALARM:
355                         return context.deserialize(jsonObject, AlarmEvent.class);
356                     case ARMING:
357                         return context.deserialize(jsonObject, ArmingEvent.class);
358                     case ERROR:
359                         return context.deserialize(jsonObject, ErrorEvent.class);
360                     case INFO:
361                         JsonElement infoType = jsonObject.get("info_type");
362                         if (infoType != null) {
363                             switch (InfoEventType.valueOf(infoType.getAsString())) {
364                                 case SECURE_ARM:
365                                     return context.deserialize(jsonObject, SecureArmInfoEvent.class);
366                                 case SUMMARY:
367                                     return context.deserialize(jsonObject, SummaryInfoEvent.class);
368                             }
369                         }
370                         break;
371                     case ZONE_EVENT:
372                         JsonElement zoneEventType = jsonObject.get("zone_event_type");
373                         if (zoneEventType != null) {
374                             switch (ZoneEventType.valueOf(zoneEventType.getAsString())) {
375                                 case ZONE_ACTIVE:
376                                     return context.deserialize(jsonObject, ZoneActiveEvent.class);
377                                 case ZONE_UPDATE:
378                                     return context.deserialize(jsonObject, ZoneUpdateEvent.class);
379                                 case ZONE_ADD:
380                                     return context.deserialize(jsonObject, ZoneAddEvent.class);
381                                 default:
382                                     break;
383                             }
384                         }
385                 }
386             }
387             return null;
388         }
389     }
390 }