]> git.basschouten.com Git - openhab-addons.git/blob
e07032d2ebd6aa58e7551d39245416e83b46e441
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.qbus.internal.protocol;
14
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.io.PrintWriter;
19 import java.net.InetAddress;
20 import java.net.Socket;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.concurrent.ExecutorService;
26 import java.util.concurrent.Executors;
27 import java.util.concurrent.TimeUnit;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.qbus.internal.QbusBridgeHandler;
32 import org.openhab.core.common.NamedThreadFactory;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.openhab.core.thing.binding.BaseThingHandler;
37 import org.openhab.core.types.Command;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 import com.google.gson.Gson;
42 import com.google.gson.GsonBuilder;
43 import com.google.gson.JsonParseException;
44
45 /**
46  * The {@link QbusCommunication} class is able to do the following tasks with Qbus
47  * CTD controllers:
48  * <ul>
49  * <li>Start and stop TCP socket connection with Qbus Server.
50  * <li>Read all the outputs and their status from the Qbus Controller.
51  * <li>Execute Qbus commands.
52  * <li>Listen to events from Qbus.
53  * </ul>
54  *
55  * A class instance is instantiated from the {@link QbusBridgeHandler} class initialization.
56  *
57  * @author Koen Schockaert - Initial Contribution
58  */
59
60 @NonNullByDefault
61 public final class QbusCommunication extends BaseThingHandler {
62
63     private final Logger logger = LoggerFactory.getLogger(QbusCommunication.class);
64
65     private @Nullable Socket qSocket;
66     private @Nullable PrintWriter qOut;
67     private @Nullable BufferedReader qIn;
68
69     private boolean listenerStopped;
70     private boolean qbusListenerRunning;
71
72     private Gson gsonOut = new Gson();
73     private Gson gsonIn;
74
75     private @Nullable String ctd;
76     private boolean ctdConnected;
77
78     private List<Map<String, String>> outputs = new ArrayList<>();
79     private final Map<Integer, QbusBistabiel> bistabiel = new HashMap<>();
80     private final Map<Integer, QbusScene> scene = new HashMap<>();
81     private final Map<Integer, QbusDimmer> dimmer = new HashMap<>();
82     private final Map<Integer, QbusRol> rol = new HashMap<>();
83     private final Map<Integer, QbusThermostat> thermostat = new HashMap<>();
84     private final Map<Integer, QbusCO2> co2 = new HashMap<>();
85
86     private final ExecutorService threadExecutor = Executors
87             .newSingleThreadExecutor(new NamedThreadFactory(getThing().getUID().getAsString(), true));
88
89     private @Nullable QbusBridgeHandler bridgeCallBack;
90
91     public QbusCommunication(Thing thing) {
92         super(thing);
93         GsonBuilder gsonBuilder = new GsonBuilder();
94         gsonBuilder.registerTypeAdapter(QbusMessageBase.class, new QbusMessageDeserializer());
95         gsonIn = gsonBuilder.create();
96     }
97
98     /**
99      * Starts main communication thread.
100      * <ul>
101      * <li>Connect to Qbus server
102      * <li>Requests outputs
103      * <li>Start listener
104      * </ul>
105      *
106      * @throws IOException
107      * @throws InterruptedException
108      */
109     public synchronized void startCommunication() throws IOException, InterruptedException {
110         QbusBridgeHandler handler = bridgeCallBack;
111         ctdConnected = false;
112
113         if (qbusListenerRunning) {
114             throw new IOException("Previous listening thread is still active.");
115         }
116
117         if (handler == null) {
118             throw new IOException("No Bridge handler initialised.");
119         }
120
121         InetAddress addr = InetAddress.getByName(handler.getAddress());
122         Integer port = handler.getPort();
123
124         if (port != null) {
125             Socket socket = new Socket(addr, port);
126             qSocket = socket;
127             qOut = new PrintWriter(socket.getOutputStream(), true);
128             qIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
129         } else {
130             return;
131         }
132
133         setSN();
134         getSN();
135
136         // Connect to Qbus server
137         connect();
138
139         // Then start thread to listen to incoming updates from Qbus.
140         threadExecutor.execute(() -> {
141             try {
142                 qbusListener();
143             } catch (IOException e) {
144                 String msg = e.getMessage();
145                 logger.warn("Could not start listening thread, IOException: {}", msg);
146             } catch (InterruptedException e) {
147                 String msg = e.getMessage();
148                 logger.warn("Could not start listening thread, InterruptedException: {}", msg);
149             }
150         });
151
152         if (!ctdConnected) {
153             handler.bridgePending("Waiting for CTD to come online...");
154         }
155     }
156
157     /**
158      * Cleanup socket when the communication with Qbus Server is closed.
159      *
160      * @throws IOException
161      *
162      */
163     public synchronized void stopCommunication() throws IOException {
164         listenerStopped = true;
165
166         Socket socket = qSocket;
167
168         if (socket != null) {
169             try {
170                 socket.close();
171             } catch (IOException ignore) {
172                 // ignore IO Error when trying to close the socket if the intention is to close it anyway
173             }
174         }
175
176         BufferedReader reader = this.qIn;
177         if (reader != null) {
178             reader.close();
179         }
180
181         PrintWriter writer = this.qOut;
182         if (writer != null) {
183             writer.close();
184         }
185
186         qSocket = null;
187         qbusListenerRunning = false;
188         ctdConnected = false;
189
190         logger.trace("Communication stopped from thread {}", Thread.currentThread().getId());
191     }
192
193     /**
194      * Close and restart communication with Qbus Server.
195      *
196      * @throws InterruptedException
197      * @throws IOException
198      */
199     public synchronized void restartCommunication() throws InterruptedException, IOException {
200         stopCommunication();
201
202         startCommunication();
203     }
204
205     /**
206      * Thread that handles incoming messages from Qbus client.
207      * <p>
208      * The thread listens to the TCP socket opened at instantiation of the {@link QbusCommunication} class
209      * and interprets all incomming json messages. It triggers state updates for active channels linked to the
210      * Qbus outputs. It is started after initialization of the communication.
211      *
212      * @return
213      * @throws IOException
214      * @throws InterruptedException
215      *
216      *
217      */
218     private void qbusListener() throws IOException, InterruptedException {
219         String qMessage;
220
221         listenerStopped = false;
222         qbusListenerRunning = true;
223
224         BufferedReader reader = this.qIn;
225
226         if (reader == null) {
227             throw new IOException("Bufferreader for incoming messages not initialized.");
228         }
229
230         try {
231             while (!Thread.currentThread().isInterrupted() && ((qMessage = reader.readLine()) != null)) {
232                 readMessage(qMessage);
233
234             }
235         } catch (IOException e) {
236             if (!listenerStopped) {
237                 qbusListenerRunning = false;
238                 // the IO has stopped working, so we need to close cleanly and try to restart
239                 restartCommunication();
240                 return;
241             }
242         } finally {
243             qbusListenerRunning = false;
244         }
245
246         if (!listenerStopped) {
247             qbusListenerRunning = false;
248
249             QbusBridgeHandler handler = bridgeCallBack;
250
251             if (handler != null) {
252                 ctdConnected = false;
253                 handler.bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, "No communication with Qbus server");
254             }
255         }
256
257         qbusListenerRunning = false;
258         logger.trace("Event listener thread stopped on thread {}", Thread.currentThread().getId());
259     };
260
261     /**
262      * Called by other methods to send json data to Qbus.
263      *
264      * @param qMessage
265      * @throws InterruptedException
266      * @throws IOException
267      */
268     synchronized void sendMessage(Object qMessage) throws InterruptedException, IOException {
269         PrintWriter writer = qOut;
270         String json = gsonOut.toJson(qMessage);
271
272         if (writer != null) {
273             writer.println(json);
274             // Delay after sending data to improve scene execution
275             TimeUnit.MILLISECONDS.sleep(250);
276         }
277
278         if ((writer == null) || (writer.checkError())) {
279             logger.warn("Error sending message, trying to restart communication");
280
281             restartCommunication();
282
283             // retry sending after restart
284             writer = qOut;
285             if (writer != null) {
286                 writer.println(json);
287             }
288             if ((writer == null) || (writer.checkError())) {
289                 logger.warn("Error resending message");
290
291             }
292         }
293     }
294
295     /**
296      * Method that interprets all feedback from Qbus Server application and calls appropriate handling methods.
297      * <ul>
298      * <li>Get request & update states for Bistabiel/Timers/Intervals/Mono outputs
299      * <li>Get request & update states for the Scenes
300      * <li>Get request & update states for Dimmers 1T and 2T
301      * <li>Get request & update states for Shutters
302      * <li>Get request & update states for Thermostats
303      * <li>Get request & update states for CO2
304      * </ul>
305      *
306      * @param qMessage message read from Qbus.
307      * @throws InterruptedException
308      * @throws IOException
309      *
310      */
311     private void readMessage(String qMessage) {
312         String sn = null;
313         String cmd = "";
314         String ctd = null;
315         Integer id = null;
316         Integer state = null;
317         Integer mode = null;
318         Double setpoint = null;
319         Double measured = null;
320         Integer slats = null;
321
322         QbusMessageBase qMessageGson;
323         try {
324             qMessageGson = gsonIn.fromJson(qMessage, QbusMessageBase.class);
325
326             if (qMessageGson != null) {
327                 ctd = qMessageGson.getSn();
328                 cmd = qMessageGson.getCmd();
329                 id = qMessageGson.getId();
330                 state = qMessageGson.getState();
331                 mode = qMessageGson.getMode();
332                 setpoint = qMessageGson.getSetPoint();
333                 measured = qMessageGson.getMeasured();
334                 slats = qMessageGson.getSlatState();
335             }
336         } catch (JsonParseException e) {
337             String msg = e.getMessage();
338             logger.trace("Not acted on unsupported json {} : {}", qMessage, msg);
339             return;
340         }
341
342         QbusBridgeHandler handler = bridgeCallBack;
343
344         if (handler != null) {
345             sn = handler.getSn();
346         }
347
348         if (sn != null && ctd != null) {
349             try {
350                 if (sn.equals(ctd) && qMessageGson != null) { // Check if commands are for this Bridge
351                     // Handle all outputs from Qbus
352                     if ("returnOutputs".equals(cmd)) {
353                         outputs = ((QbusMessageListMap) qMessageGson).getOutputs();
354
355                         for (Map<String, String> ctdOutputs : outputs) {
356
357                             String ctdType = ctdOutputs.get("type");
358                             String ctdIdStr = ctdOutputs.get("id");
359                             Integer ctdId = null;
360
361                             if (ctdIdStr != null) {
362                                 ctdId = Integer.parseInt(ctdIdStr);
363                             } else {
364                                 return;
365                             }
366
367                             if (ctdType != null) {
368                                 String ctdState = ctdOutputs.get("state");
369                                 String ctdMmode = ctdOutputs.get("regime");
370                                 String ctdSetpoint = ctdOutputs.get("setpoint");
371                                 String ctdMeasured = ctdOutputs.get("measured");
372                                 String ctdSlats = ctdOutputs.get("slats");
373
374                                 Integer ctdStateI = null;
375                                 if (ctdState != null) {
376                                     ctdStateI = Integer.parseInt(ctdState);
377                                 }
378
379                                 Integer ctdSlatsI = null;
380                                 if (ctdSlats != null) {
381                                     ctdSlatsI = Integer.parseInt(ctdSlats);
382                                 }
383
384                                 Integer ctdMmodeI = null;
385                                 if (ctdMmode != null) {
386                                     ctdMmodeI = Integer.parseInt(ctdMmode);
387                                 }
388
389                                 Double ctdSetpointD = null;
390                                 if (ctdSetpoint != null) {
391                                     ctdSetpointD = Double.parseDouble(ctdSetpoint);
392                                 }
393
394                                 Double ctdMeasuredD = null;
395                                 if (ctdMeasured != null) {
396                                     ctdMeasuredD = Double.parseDouble(ctdMeasured);
397                                 }
398
399                                 if (ctdState != null) {
400                                     if (ctdType.equals("bistabiel")) {
401                                         QbusBistabiel output = new QbusBistabiel(ctdId);
402                                         if (!bistabiel.containsKey(ctdId)) {
403                                             output.setQComm(this);
404                                             output.updateState(ctdStateI);
405                                             bistabiel.put(ctdId, output);
406                                         } else {
407                                             output.updateState(ctdStateI);
408                                         }
409                                     } else if (ctdType.equals("dimmer")) {
410                                         QbusDimmer output = new QbusDimmer(ctdId);
411                                         if (!dimmer.containsKey(ctdId)) {
412                                             output.setQComm(this);
413                                             output.updateState(ctdStateI);
414                                             dimmer.put(ctdId, output);
415                                         } else {
416                                             output.updateState(ctdStateI);
417                                         }
418                                     } else if (ctdType.equals("CO2")) {
419                                         QbusCO2 output = new QbusCO2();
420                                         if (!co2.containsKey(ctdId)) {
421                                             output.updateState(ctdStateI);
422                                             co2.put(ctdId, output);
423                                         } else {
424                                             output.updateState(ctdStateI);
425                                         }
426                                     } else if (ctdType.equals("scene")) {
427                                         QbusScene output = new QbusScene(ctdId);
428                                         if (!scene.containsKey(ctdId)) {
429                                             output.setQComm(this);
430                                             scene.put(ctdId, output);
431                                         }
432                                     } else if (ctdType.equals("rol")) {
433                                         QbusRol output = new QbusRol(ctdId);
434                                         if (!rol.containsKey(ctdId)) {
435                                             output.setQComm(this);
436                                             output.updateState(ctdStateI);
437                                             if (ctdSlats != null) {
438                                                 output.updateSlats(ctdSlatsI);
439                                             }
440                                             rol.put(ctdId, output);
441                                         } else {
442                                             output.updateState(ctdStateI);
443                                             if (ctdSlats != null) {
444                                                 output.updateSlats(ctdSlatsI);
445                                             }
446                                         }
447                                     }
448                                 } else if (ctdMeasuredD != null && ctdSetpointD != null && ctdMmodeI != null) {
449                                     if (ctdType.equals("thermostat")) {
450                                         QbusThermostat output = new QbusThermostat(ctdId);
451                                         if (!thermostat.containsKey(ctdId)) {
452                                             output.setQComm(this);
453                                             output.updateState(ctdMeasuredD, ctdSetpointD, ctdMmodeI);
454                                             thermostat.put(ctdId, output);
455                                         } else {
456                                             output.updateState(ctdMeasuredD, ctdSetpointD, ctdMmodeI);
457                                         }
458                                     }
459                                 }
460                             }
461                         }
462                         // Handle update commands from Qbus
463                     } else if ("updateBistabiel".equals(cmd)) {
464                         if (id != null && state != null) {
465                             updateBistabiel(id, state);
466                         }
467                     } else if ("updateDimmer".equals(cmd)) {
468                         if (id != null && state != null) {
469                             updateDimmer(id, state);
470                         }
471                     } else if ("updateDimmer".equals(cmd)) {
472                         if (id != null && state != null) {
473                             updateDimmer(id, state);
474                         }
475                     } else if ("updateCo2".equals(cmd)) {
476                         if (id != null && state != null) {
477                             updateCO2(id, state);
478                         }
479                     } else if ("updateThermostat".equals(cmd)) {
480                         if (id != null && measured != null && setpoint != null && mode != null) {
481                             updateThermostat(id, mode, setpoint, measured);
482                         }
483                     } else if ("updateRol02p".equals(cmd)) {
484                         if (id != null && state != null) {
485                             updateRol(id, state);
486                         }
487                     } else if ("updateRol02pSlat".equals(cmd)) {
488                         if (id != null && state != null && slats != null) {
489                             updateRolSlats(id, state, slats);
490                         }
491                         // Incomming commands from Qbus server to verify the client connection
492                     } else if ("noconnection".equals(cmd)) {
493                         eventDisconnect();
494                     } else if ("connected".equals(cmd)) {
495                         // threadExecutor.execute(() -> {
496                         try {
497                             requestOutputs();
498                         } catch (InterruptedException e) {
499                             String msg = e.getMessage();
500                             logger.warn("Could not request outputs. InterruptedException: {}", msg);
501                         } catch (IOException e) {
502                             String msg = e.getMessage();
503                             logger.warn("Could not request outputs. IOException: {}", msg);
504                         }
505                     }
506                 }
507             } catch (JsonParseException e) {
508                 String msg = e.getMessage();
509                 logger.warn("Not acted on unsupported json {}, {}", qMessage, msg);
510             }
511         }
512     }
513
514     /**
515      * Initialize the communication object
516      */
517     @Override
518     public void initialize() {
519     }
520
521     /**
522      * Initial connection to Qbus Server to open a communication channel
523      *
524      * @throws InterruptedException
525      * @throws IOException
526      */
527     private void connect() throws InterruptedException, IOException {
528         String snr = getSN();
529
530         if (snr != null) {
531             QbusMessageCmd qCmd = new QbusMessageCmd(snr, "openHAB");
532
533             sendMessage(qCmd);
534
535             BufferedReader reader = qIn;
536
537             if (reader == null) {
538                 throw new IOException("Cannot read from socket, reader not connected.");
539             }
540             readMessage(reader.readLine());
541
542         } else {
543             QbusBridgeHandler handler = bridgeCallBack;
544             if (handler != null) {
545                 handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined");
546             }
547         }
548     }
549
550     /**
551      * Send a request for all available outputs and initializes them via readMessage
552      *
553      * @throws InterruptedException
554      * @throws IOException
555      */
556     private void requestOutputs() throws InterruptedException, IOException {
557         String snr = getSN();
558         QbusBridgeHandler handler = bridgeCallBack;
559
560         if (snr != null) {
561             QbusMessageCmd qCmd = new QbusMessageCmd(snr, "all");
562             sendMessage(qCmd);
563
564             BufferedReader reader = qIn;
565             if (reader == null) {
566                 throw new IOException("Cannot read from socket, reader not connected.");
567             }
568             readMessage(reader.readLine());
569             ctdConnected = true;
570
571             if (handler != null) {
572                 handler.bridgeOnline();
573             }
574
575         } else {
576             if (handler != null) {
577                 handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined");
578             }
579         }
580     }
581
582     /**
583      * Event on incoming Bistabiel/Timer/Mono/Interval updates
584      *
585      * @param id
586      * @param state
587      */
588     private void updateBistabiel(Integer id, Integer state) {
589         QbusBistabiel qBistabiel = this.bistabiel.get(id);
590
591         if (qBistabiel != null) {
592             qBistabiel.updateState(state);
593         } else {
594             logger.trace("Bistabiel in controller not known {}", id);
595         }
596     }
597
598     /**
599      * Event on incoming Dimmer updates
600      *
601      * @param id
602      * @param state
603      */
604     private void updateDimmer(Integer id, Integer state) {
605         QbusDimmer qDimmer = this.dimmer.get(id);
606
607         if (qDimmer != null) {
608             qDimmer.updateState(state);
609         } else {
610             logger.trace("Dimmer in controller not known {}", id);
611         }
612     }
613
614     /**
615      * Event on incoming thermostat updates
616      *
617      * @param id
618      * @param mode
619      * @param sp
620      * @param ct
621      */
622     private void updateThermostat(Integer id, int mode, double sp, double ct) {
623         QbusThermostat qThermostat = this.thermostat.get(id);
624
625         if (qThermostat != null) {
626             qThermostat.updateState(ct, sp, mode);
627         } else {
628             logger.trace("Thermostat in controller not known {}", id);
629         }
630     }
631
632     /**
633      * Event on incoming CO2 updates
634      *
635      * @param id
636      * @param state
637      */
638     private void updateCO2(Integer id, Integer state) {
639         QbusCO2 qCO2 = this.co2.get(id);
640
641         if (qCO2 != null) {
642             qCO2.updateState(state);
643         } else {
644             logger.trace("CO2 in controller not known {}", id);
645         }
646     }
647
648     /**
649      * Event on incoming screen updates
650      *
651      * @param id
652      * @param state
653      */
654     private void updateRol(Integer id, Integer state) {
655         QbusRol qRol = this.rol.get(id);
656
657         if (qRol != null) {
658             qRol.updateState(state);
659         } else {
660             logger.trace("ROL02P in controller not known {}", id);
661         }
662     }
663
664     /**
665      * Event on incoming screen with slats updates
666      *
667      * @param id
668      * @param state
669      * @param slats
670      */
671     private void updateRolSlats(Integer id, Integer state, Integer slats) {
672         QbusRol qRol = this.rol.get(id);
673
674         if (qRol != null) {
675             qRol.updateState(state);
676             qRol.updateSlats(slats);
677         } else {
678             logger.trace("ROL02P with slats in controller not known {}", id);
679         }
680     }
681
682     /**
683      * Put Bridge offline when there is no connection from the QbusClient
684      *
685      */
686     private void eventDisconnect() {
687         QbusBridgeHandler handler = bridgeCallBack;
688
689         if (handler != null) {
690             handler.bridgePending("Waiting for CTD connection");
691         }
692     }
693
694     /**
695      * Return all Bistabiel/Timers/Mono/Intervals in the Qbus Controller.
696      *
697      * @return
698      */
699     public Map<Integer, QbusBistabiel> getBistabiel() {
700         return this.bistabiel;
701     }
702
703     /**
704      * Return all Scenes in the Qbus Controller
705      *
706      * @return
707      */
708     public Map<Integer, QbusScene> getScene() {
709         return this.scene;
710     }
711
712     /**
713      * Return all Dimmers outputs in the Qbus Controller.
714      *
715      * @return
716      */
717     public Map<Integer, QbusDimmer> getDimmer() {
718         return this.dimmer;
719     }
720
721     /**
722      * Return all rollershutter/screen outputs in the Qbus Controller.
723      *
724      * @return
725      */
726     public Map<Integer, QbusRol> getRol() {
727         return this.rol;
728     }
729
730     /**
731      * Return all Thermostats outputs in the Qbus Controller.
732      *
733      * @return
734      */
735     public Map<Integer, QbusThermostat> getThermostat() {
736         return this.thermostat;
737     }
738
739     /**
740      * Return all CO2 outputs in the Qbus Controller.
741      *
742      * @return
743      */
744     public Map<Integer, QbusCO2> getCo2() {
745         return this.co2;
746     }
747
748     /**
749      * Method to check if communication with Qbus Server is active
750      *
751      * @return True if active
752      */
753     public boolean communicationActive() {
754         return qSocket != null;
755     }
756
757     /**
758      * Method to check if communication with Qbus Client is active
759      *
760      * @return True if active
761      */
762     public boolean clientConnected() {
763         return ctdConnected;
764     }
765
766     /**
767      * @param bridgeCallBack the bridgeCallBack to set
768      */
769     public void setBridgeCallBack(QbusBridgeHandler bridgeCallBack) {
770         this.bridgeCallBack = bridgeCallBack;
771     }
772
773     /**
774      * Get the serial number of the CTD as configured in the Bridge.
775      *
776      * @return serial number of controller
777      */
778     public @Nullable String getSN() {
779         return this.ctd;
780     }
781
782     /**
783      * Sets the serial number of the CTD, as configured in the Bridge.
784      */
785     public void setSN() {
786         QbusBridgeHandler qBridgeHandler = bridgeCallBack;
787
788         if (qBridgeHandler != null) {
789             this.ctd = qBridgeHandler.getSn();
790         }
791     }
792
793     @Override
794     public void handleCommand(ChannelUID channelUID, Command command) {
795     }
796 }