2 * Copyright (c) 2010-2021 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.qbus.internal.protocol;
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;
25 import java.util.concurrent.ExecutorService;
26 import java.util.concurrent.Executors;
27 import java.util.concurrent.TimeUnit;
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;
41 import com.google.gson.Gson;
42 import com.google.gson.GsonBuilder;
43 import com.google.gson.JsonParseException;
46 * The {@link QbusCommunication} class is able to do the following tasks with Qbus
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.
55 * A class instance is instantiated from the {@link QbusBridgeHandler} class initialization.
57 * @author Koen Schockaert - Initial Contribution
61 public final class QbusCommunication extends BaseThingHandler {
63 private final Logger logger = LoggerFactory.getLogger(QbusCommunication.class);
65 private @Nullable Socket qSocket;
66 private @Nullable PrintWriter qOut;
67 private @Nullable BufferedReader qIn;
69 private boolean listenerStopped;
70 private boolean qbusListenerRunning;
72 private Gson gsonOut = new Gson();
75 private @Nullable String ctd;
76 private boolean ctdConnected;
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<>();
86 private final ExecutorService threadExecutor = Executors
87 .newSingleThreadExecutor(new NamedThreadFactory(getThing().getUID().getAsString(), true));
89 private @Nullable QbusBridgeHandler bridgeCallBack;
91 public QbusCommunication(Thing thing) {
93 GsonBuilder gsonBuilder = new GsonBuilder();
94 gsonBuilder.registerTypeAdapter(QbusMessageBase.class, new QbusMessageDeserializer());
95 gsonIn = gsonBuilder.create();
99 * Starts main communication thread.
101 * <li>Connect to Qbus server
102 * <li>Requests outputs
106 * @throws IOException
107 * @throws InterruptedException
109 public synchronized void startCommunication() throws IOException, InterruptedException {
110 QbusBridgeHandler handler = bridgeCallBack;
111 ctdConnected = false;
113 if (qbusListenerRunning) {
114 throw new IOException("Previous listening thread is still active.");
117 if (handler == null) {
118 throw new IOException("No Bridge handler initialised.");
121 InetAddress addr = InetAddress.getByName(handler.getAddress());
122 Integer port = handler.getPort();
125 Socket socket = new Socket(addr, port);
127 qOut = new PrintWriter(socket.getOutputStream(), true);
128 qIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
136 // Connect to Qbus server
139 // Then start thread to listen to incoming updates from Qbus.
140 threadExecutor.execute(() -> {
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);
153 handler.bridgePending("Waiting for CTD to come online...");
158 * Cleanup socket when the communication with Qbus Server is closed.
160 * @throws IOException
163 public synchronized void stopCommunication() throws IOException {
164 listenerStopped = true;
166 Socket socket = qSocket;
168 if (socket != null) {
171 } catch (IOException ignore) {
172 // ignore IO Error when trying to close the socket if the intention is to close it anyway
176 BufferedReader reader = this.qIn;
177 if (reader != null) {
181 PrintWriter writer = this.qOut;
182 if (writer != null) {
187 qbusListenerRunning = false;
188 ctdConnected = false;
190 logger.trace("Communication stopped from thread {}", Thread.currentThread().getId());
194 * Close and restart communication with Qbus Server.
196 * @throws InterruptedException
197 * @throws IOException
199 public synchronized void restartCommunication() throws InterruptedException, IOException {
202 startCommunication();
206 * Thread that handles incoming messages from Qbus client.
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.
213 * @throws IOException
214 * @throws InterruptedException
218 private void qbusListener() throws IOException, InterruptedException {
221 listenerStopped = false;
222 qbusListenerRunning = true;
224 BufferedReader reader = this.qIn;
226 if (reader == null) {
227 throw new IOException("Bufferreader for incoming messages not initialized.");
231 while (!Thread.currentThread().isInterrupted() && ((qMessage = reader.readLine()) != null)) {
232 readMessage(qMessage);
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();
243 qbusListenerRunning = false;
246 if (!listenerStopped) {
247 qbusListenerRunning = false;
249 QbusBridgeHandler handler = bridgeCallBack;
251 if (handler != null) {
252 ctdConnected = false;
253 handler.bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, "No communication with Qbus server");
257 qbusListenerRunning = false;
258 logger.trace("Event listener thread stopped on thread {}", Thread.currentThread().getId());
262 * Called by other methods to send json data to Qbus.
265 * @throws InterruptedException
266 * @throws IOException
268 synchronized void sendMessage(Object qMessage) throws InterruptedException, IOException {
269 PrintWriter writer = qOut;
270 String json = gsonOut.toJson(qMessage);
272 if (writer != null) {
273 writer.println(json);
274 // Delay after sending data to improve scene execution
275 TimeUnit.MILLISECONDS.sleep(250);
278 if ((writer == null) || (writer.checkError())) {
279 logger.warn("Error sending message, trying to restart communication");
281 restartCommunication();
283 // retry sending after restart
285 if (writer != null) {
286 writer.println(json);
288 if ((writer == null) || (writer.checkError())) {
289 logger.warn("Error resending message");
296 * Method that interprets all feedback from Qbus Server application and calls appropriate handling methods.
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
306 * @param qMessage message read from Qbus.
307 * @throws InterruptedException
308 * @throws IOException
311 private void readMessage(String qMessage) {
316 Integer state = null;
318 Double setpoint = null;
319 Double measured = null;
320 Integer slats = null;
322 QbusMessageBase qMessageGson;
324 qMessageGson = gsonIn.fromJson(qMessage, QbusMessageBase.class);
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();
336 } catch (JsonParseException e) {
337 String msg = e.getMessage();
338 logger.trace("Not acted on unsupported json {} : {}", qMessage, msg);
342 QbusBridgeHandler handler = bridgeCallBack;
344 if (handler != null) {
345 sn = handler.getSn();
348 if (sn != null && ctd != null) {
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();
355 for (Map<String, String> ctdOutputs : outputs) {
357 String ctdType = ctdOutputs.get("type");
358 String ctdIdStr = ctdOutputs.get("id");
359 Integer ctdId = null;
361 if (ctdIdStr != null) {
362 ctdId = Integer.parseInt(ctdIdStr);
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");
374 Integer ctdStateI = null;
375 if (ctdState != null) {
376 ctdStateI = Integer.parseInt(ctdState);
379 Integer ctdSlatsI = null;
380 if (ctdSlats != null) {
381 ctdSlatsI = Integer.parseInt(ctdSlats);
384 Integer ctdMmodeI = null;
385 if (ctdMmode != null) {
386 ctdMmodeI = Integer.parseInt(ctdMmode);
389 Double ctdSetpointD = null;
390 if (ctdSetpoint != null) {
391 ctdSetpointD = Double.parseDouble(ctdSetpoint);
394 Double ctdMeasuredD = null;
395 if (ctdMeasured != null) {
396 ctdMeasuredD = Double.parseDouble(ctdMeasured);
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);
407 output.updateState(ctdStateI);
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);
416 output.updateState(ctdStateI);
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);
424 output.updateState(ctdStateI);
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);
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);
440 rol.put(ctdId, output);
442 output.updateState(ctdStateI);
443 if (ctdSlats != null) {
444 output.updateSlats(ctdSlatsI);
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);
456 output.updateState(ctdMeasuredD, ctdSetpointD, ctdMmodeI);
462 // Handle update commands from Qbus
463 } else if ("updateBistabiel".equals(cmd)) {
464 if (id != null && state != null) {
465 updateBistabiel(id, state);
467 } else if ("updateDimmer".equals(cmd)) {
468 if (id != null && state != null) {
469 updateDimmer(id, state);
471 } else if ("updateDimmer".equals(cmd)) {
472 if (id != null && state != null) {
473 updateDimmer(id, state);
475 } else if ("updateCo2".equals(cmd)) {
476 if (id != null && state != null) {
477 updateCO2(id, state);
479 } else if ("updateThermostat".equals(cmd)) {
480 if (id != null && measured != null && setpoint != null && mode != null) {
481 updateThermostat(id, mode, setpoint, measured);
483 } else if ("updateRol02p".equals(cmd)) {
484 if (id != null && state != null) {
485 updateRol(id, state);
487 } else if ("updateRol02pSlat".equals(cmd)) {
488 if (id != null && state != null && slats != null) {
489 updateRolSlats(id, state, slats);
491 // Incomming commands from Qbus server to verify the client connection
492 } else if ("noconnection".equals(cmd)) {
494 } else if ("connected".equals(cmd)) {
495 // threadExecutor.execute(() -> {
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);
507 } catch (JsonParseException e) {
508 String msg = e.getMessage();
509 logger.warn("Not acted on unsupported json {}, {}", qMessage, msg);
515 * Initialize the communication object
518 public void initialize() {
522 * Initial connection to Qbus Server to open a communication channel
524 * @throws InterruptedException
525 * @throws IOException
527 private void connect() throws InterruptedException, IOException {
528 String snr = getSN();
531 QbusMessageCmd qCmd = new QbusMessageCmd(snr, "openHAB");
535 BufferedReader reader = qIn;
537 if (reader == null) {
538 throw new IOException("Cannot read from socket, reader not connected.");
540 readMessage(reader.readLine());
543 QbusBridgeHandler handler = bridgeCallBack;
544 if (handler != null) {
545 handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined");
551 * Send a request for all available outputs and initializes them via readMessage
553 * @throws InterruptedException
554 * @throws IOException
556 private void requestOutputs() throws InterruptedException, IOException {
557 String snr = getSN();
558 QbusBridgeHandler handler = bridgeCallBack;
561 QbusMessageCmd qCmd = new QbusMessageCmd(snr, "all");
564 BufferedReader reader = qIn;
565 if (reader == null) {
566 throw new IOException("Cannot read from socket, reader not connected.");
568 readMessage(reader.readLine());
571 if (handler != null) {
572 handler.bridgeOnline();
576 if (handler != null) {
577 handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined");
583 * Event on incoming Bistabiel/Timer/Mono/Interval updates
588 private void updateBistabiel(Integer id, Integer state) {
589 QbusBistabiel qBistabiel = this.bistabiel.get(id);
591 if (qBistabiel != null) {
592 qBistabiel.updateState(state);
594 logger.trace("Bistabiel in controller not known {}", id);
599 * Event on incoming Dimmer updates
604 private void updateDimmer(Integer id, Integer state) {
605 QbusDimmer qDimmer = this.dimmer.get(id);
607 if (qDimmer != null) {
608 qDimmer.updateState(state);
610 logger.trace("Dimmer in controller not known {}", id);
615 * Event on incoming thermostat updates
622 private void updateThermostat(Integer id, int mode, double sp, double ct) {
623 QbusThermostat qThermostat = this.thermostat.get(id);
625 if (qThermostat != null) {
626 qThermostat.updateState(ct, sp, mode);
628 logger.trace("Thermostat in controller not known {}", id);
633 * Event on incoming CO2 updates
638 private void updateCO2(Integer id, Integer state) {
639 QbusCO2 qCO2 = this.co2.get(id);
642 qCO2.updateState(state);
644 logger.trace("CO2 in controller not known {}", id);
649 * Event on incoming screen updates
654 private void updateRol(Integer id, Integer state) {
655 QbusRol qRol = this.rol.get(id);
658 qRol.updateState(state);
660 logger.trace("ROL02P in controller not known {}", id);
665 * Event on incoming screen with slats updates
671 private void updateRolSlats(Integer id, Integer state, Integer slats) {
672 QbusRol qRol = this.rol.get(id);
675 qRol.updateState(state);
676 qRol.updateSlats(slats);
678 logger.trace("ROL02P with slats in controller not known {}", id);
683 * Put Bridge offline when there is no connection from the QbusClient
686 private void eventDisconnect() {
687 QbusBridgeHandler handler = bridgeCallBack;
689 if (handler != null) {
690 handler.bridgePending("Waiting for CTD connection");
695 * Return all Bistabiel/Timers/Mono/Intervals in the Qbus Controller.
699 public Map<Integer, QbusBistabiel> getBistabiel() {
700 return this.bistabiel;
704 * Return all Scenes in the Qbus Controller
708 public Map<Integer, QbusScene> getScene() {
713 * Return all Dimmers outputs in the Qbus Controller.
717 public Map<Integer, QbusDimmer> getDimmer() {
722 * Return all rollershutter/screen outputs in the Qbus Controller.
726 public Map<Integer, QbusRol> getRol() {
731 * Return all Thermostats outputs in the Qbus Controller.
735 public Map<Integer, QbusThermostat> getThermostat() {
736 return this.thermostat;
740 * Return all CO2 outputs in the Qbus Controller.
744 public Map<Integer, QbusCO2> getCo2() {
749 * Method to check if communication with Qbus Server is active
751 * @return True if active
753 public boolean communicationActive() {
754 return qSocket != null;
758 * Method to check if communication with Qbus Client is active
760 * @return True if active
762 public boolean clientConnected() {
767 * @param bridgeCallBack the bridgeCallBack to set
769 public void setBridgeCallBack(QbusBridgeHandler bridgeCallBack) {
770 this.bridgeCallBack = bridgeCallBack;
774 * Get the serial number of the CTD as configured in the Bridge.
776 * @return serial number of controller
778 public @Nullable String getSN() {
783 * Sets the serial number of the CTD, as configured in the Bridge.
785 public void setSN() {
786 QbusBridgeHandler qBridgeHandler = bridgeCallBack;
788 if (qBridgeHandler != null) {
789 this.ctd = qBridgeHandler.getSn();
794 public void handleCommand(ChannelUID channelUID, Command command) {