2 * Copyright (c) 2010-2024 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 handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Please set a correct port.");
130 handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Please set the hostname of the Qbus server.");
134 Socket socket = null;
137 socket = new Socket(addr, port);
139 qOut = new PrintWriter(socket.getOutputStream(), true);
140 qIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
141 } catch (IOException e) {
142 String msg = e.getMessage();
143 handler.bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, "No communication with Qbus Server. " + msg);
150 // Connect to Qbus server
153 // Then start thread to listen to incoming updates from Qbus.
154 threadExecutor.execute(() -> {
157 } catch (IOException e) {
158 String msg = e.getMessage();
159 logger.warn("Could not start listening thread, IOException: {}", msg);
160 } catch (InterruptedException e) {
161 String msg = e.getMessage();
162 logger.warn("Could not start listening thread, InterruptedException: {}", msg);
167 handler.bridgePending("Waiting for CTD to come online...");
172 * Cleanup socket when the communication with Qbus Server is closed.
174 * @throws IOException
177 public synchronized void stopCommunication() throws IOException {
178 listenerStopped = true;
180 Socket socket = qSocket;
182 if (socket != null) {
185 } catch (IOException ignore) {
186 // ignore IO Error when trying to close the socket if the intention is to close it anyway
190 BufferedReader reader = this.qIn;
191 if (reader != null) {
195 PrintWriter writer = this.qOut;
196 if (writer != null) {
201 qbusListenerRunning = false;
202 ctdConnected = false;
204 logger.trace("Communication stopped from thread {}", Thread.currentThread().getId());
208 * Close and restart communication with Qbus Server.
210 * @throws InterruptedException
211 * @throws IOException
213 public synchronized void restartCommunication() throws InterruptedException, IOException {
216 startCommunication();
220 * Thread that handles incoming messages from Qbus client.
222 * The thread listens to the TCP socket opened at instantiation of the {@link QbusCommunication} class
223 * and interprets all incomming json messages. It triggers state updates for active channels linked to the
224 * Qbus outputs. It is started after initialization of the communication.
227 * @throws IOException
228 * @throws InterruptedException
232 private void qbusListener() throws IOException, InterruptedException {
235 listenerStopped = false;
236 qbusListenerRunning = true;
238 BufferedReader reader = this.qIn;
240 if (reader == null) {
241 throw new IOException("Bufferreader for incoming messages not initialized.");
245 while (!Thread.currentThread().isInterrupted() && ((qMessage = reader.readLine()) != null)) {
246 readMessage(qMessage);
249 } catch (IOException e) {
250 if (!listenerStopped) {
251 qbusListenerRunning = false;
252 // the IO has stopped working, so we need to close cleanly and try to restart
253 restartCommunication();
257 qbusListenerRunning = false;
260 if (!listenerStopped) {
261 qbusListenerRunning = false;
263 QbusBridgeHandler handler = bridgeCallBack;
265 if (handler != null) {
266 ctdConnected = false;
267 handler.bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, "No communication with Qbus server");
271 qbusListenerRunning = false;
272 logger.trace("Event listener thread stopped on thread {}", Thread.currentThread().getId());
276 * Called by other methods to send json data to Qbus.
279 * @throws InterruptedException
280 * @throws IOException
282 synchronized void sendMessage(Object qMessage) throws InterruptedException, IOException {
283 PrintWriter writer = qOut;
284 String json = gsonOut.toJson(qMessage);
286 if (writer != null) {
287 writer.println(json);
288 // Delay after sending data to improve scene execution
289 TimeUnit.MILLISECONDS.sleep(250);
292 if ((writer == null) || (writer.checkError())) {
293 logger.warn("Error sending message, trying to restart communication");
295 restartCommunication();
297 // retry sending after restart
299 if (writer != null) {
300 writer.println(json);
302 if ((writer == null) || (writer.checkError())) {
303 logger.warn("Error resending message");
310 * Method that interprets all feedback from Qbus Server application and calls appropriate handling methods.
312 * <li>Get request & update states for Bistabiel/Timers/Intervals/Mono outputs
313 * <li>Get request & update states for the Scenes
314 * <li>Get request & update states for Dimmers 1T and 2T
315 * <li>Get request & update states for Shutters
316 * <li>Get request & update states for Thermostats
317 * <li>Get request & update states for CO2
320 * @param qMessage message read from Qbus.
321 * @throws InterruptedException
322 * @throws IOException
325 private void readMessage(String qMessage) {
330 Integer state = null;
332 Double setpoint = null;
333 Double measured = null;
334 Integer slats = null;
336 QbusMessageBase qMessageGson;
338 qMessageGson = gsonIn.fromJson(qMessage, QbusMessageBase.class);
340 if (qMessageGson != null) {
341 ctd = qMessageGson.getSn();
342 cmd = qMessageGson.getCmd();
343 id = qMessageGson.getId();
344 state = qMessageGson.getState();
345 mode = qMessageGson.getMode();
346 setpoint = qMessageGson.getSetPoint();
347 measured = qMessageGson.getMeasured();
348 slats = qMessageGson.getSlatState();
350 } catch (JsonParseException e) {
351 String msg = e.getMessage();
352 logger.trace("Not acted on unsupported json {} : {}", qMessage, msg);
355 QbusBridgeHandler handler = bridgeCallBack;
357 if (handler != null) {
358 sn = handler.getSn();
361 if (sn != null && ctd != null) {
363 if (sn.equals(ctd) && qMessageGson != null) { // Check if commands are for this Bridge
364 // Handle all outputs from Qbus
365 if ("returnOutputs".equals(cmd)) {
366 outputs = ((QbusMessageListMap) qMessageGson).getOutputs();
368 for (Map<String, String> ctdOutputs : outputs) {
370 String ctdType = ctdOutputs.get("type");
371 String ctdIdStr = ctdOutputs.get("id");
372 Integer ctdId = null;
374 if (ctdIdStr != null) {
375 ctdId = Integer.parseInt(ctdIdStr);
380 if (ctdType != null) {
381 String ctdState = ctdOutputs.get("state");
382 String ctdMmode = ctdOutputs.get("regime");
383 String ctdSetpoint = ctdOutputs.get("setpoint");
384 String ctdMeasured = ctdOutputs.get("measured");
385 String ctdSlats = ctdOutputs.get("slats");
387 Integer ctdStateI = null;
388 if (ctdState != null) {
389 ctdStateI = Integer.parseInt(ctdState);
392 Integer ctdSlatsI = null;
393 if (ctdSlats != null) {
394 ctdSlatsI = Integer.parseInt(ctdSlats);
397 Integer ctdMmodeI = null;
398 if (ctdMmode != null) {
399 ctdMmodeI = Integer.parseInt(ctdMmode);
402 Double ctdSetpointD = null;
403 if (ctdSetpoint != null) {
404 ctdSetpointD = Double.parseDouble(ctdSetpoint);
407 Double ctdMeasuredD = null;
408 if (ctdMeasured != null) {
409 ctdMeasuredD = Double.parseDouble(ctdMeasured);
412 if (ctdState != null) {
413 if ("bistabiel".equals(ctdType)) {
414 QbusBistabiel output = new QbusBistabiel(ctdId);
415 if (!bistabiel.containsKey(ctdId)) {
416 output.setQComm(this);
417 output.updateState(ctdStateI);
418 bistabiel.put(ctdId, output);
420 output.updateState(ctdStateI);
422 } else if ("dimmer".equals(ctdType)) {
423 QbusDimmer output = new QbusDimmer(ctdId);
424 if (!dimmer.containsKey(ctdId)) {
425 output.setQComm(this);
426 output.updateState(ctdStateI);
427 dimmer.put(ctdId, output);
429 output.updateState(ctdStateI);
431 } else if ("CO2".equals(ctdType)) {
432 QbusCO2 output = new QbusCO2();
433 if (!co2.containsKey(ctdId)) {
434 output.updateState(ctdStateI);
435 co2.put(ctdId, output);
437 output.updateState(ctdStateI);
439 } else if ("scene".equals(ctdType)) {
440 QbusScene output = new QbusScene(ctdId);
441 if (!scene.containsKey(ctdId)) {
442 output.setQComm(this);
443 scene.put(ctdId, output);
445 } else if ("rol".equals(ctdType)) {
446 QbusRol output = new QbusRol(ctdId);
447 if (!rol.containsKey(ctdId)) {
448 output.setQComm(this);
449 output.updateState(ctdStateI);
450 if (ctdSlats != null) {
451 output.updateSlats(ctdSlatsI);
453 rol.put(ctdId, output);
455 output.updateState(ctdStateI);
456 if (ctdSlats != null) {
457 output.updateSlats(ctdSlatsI);
461 } else if (ctdMeasuredD != null && ctdSetpointD != null && ctdMmodeI != null) {
462 if ("thermostat".equals(ctdType)) {
463 QbusThermostat output = new QbusThermostat(ctdId);
464 if (!thermostat.containsKey(ctdId)) {
465 output.setQComm(this);
466 output.updateState(ctdMeasuredD, ctdSetpointD, ctdMmodeI);
467 thermostat.put(ctdId, output);
469 output.updateState(ctdMeasuredD, ctdSetpointD, ctdMmodeI);
475 // Handle update commands from Qbus
476 } else if ("updateBistabiel".equals(cmd)) {
477 if (id != null && state != null) {
478 updateBistabiel(id, state);
480 } else if ("updateDimmer".equals(cmd)) {
481 if (id != null && state != null) {
482 updateDimmer(id, state);
484 } else if ("updateDimmer".equals(cmd)) {
485 if (id != null && state != null) {
486 updateDimmer(id, state);
488 } else if ("updateCo2".equals(cmd)) {
489 if (id != null && state != null) {
490 updateCO2(id, state);
492 } else if ("updateThermostat".equals(cmd)) {
493 if (id != null && measured != null && setpoint != null && mode != null) {
494 updateThermostat(id, mode, setpoint, measured);
496 } else if ("updateRol02p".equals(cmd)) {
497 if (id != null && state != null) {
498 updateRol(id, state);
500 } else if ("updateRol02pSlat".equals(cmd)) {
501 if (id != null && state != null && slats != null) {
502 updateRolSlats(id, state, slats);
504 // Incomming commands from Qbus server to verify the client connection
505 } else if ("noconnection".equals(cmd)) {
507 } else if ("connected".equals(cmd)) {
508 // threadExecutor.execute(() -> {
511 } catch (InterruptedException e) {
512 String msg = e.getMessage();
513 logger.warn("Could not request outputs. InterruptedException: {}", msg);
514 } catch (IOException e) {
515 String msg = e.getMessage();
516 logger.warn("Could not request outputs. IOException: {}", msg);
520 } catch (JsonParseException e) {
521 String msg = e.getMessage();
522 logger.warn("Not acted on unsupported json {}, {}", qMessage, msg);
528 * Initialize the communication object
531 public void initialize() {
535 * Initial connection to Qbus Server to open a communication channel
537 * @throws InterruptedException
538 * @throws IOException
540 private void connect() throws InterruptedException, IOException {
541 String snr = getSN();
544 QbusMessageCmd qCmd = new QbusMessageCmd(snr, "openHAB");
548 BufferedReader reader = qIn;
550 if (reader == null) {
551 throw new IOException("Cannot read from socket, reader not connected.");
553 readMessage(reader.readLine());
556 QbusBridgeHandler handler = bridgeCallBack;
557 if (handler != null) {
558 handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined");
564 * Send a request for all available outputs and initializes them via readMessage
566 * @throws InterruptedException
567 * @throws IOException
569 private void requestOutputs() throws InterruptedException, IOException {
570 String snr = getSN();
571 QbusBridgeHandler handler = bridgeCallBack;
574 QbusMessageCmd qCmd = new QbusMessageCmd(snr, "all");
577 BufferedReader reader = qIn;
578 if (reader == null) {
579 throw new IOException("Cannot read from socket, reader not connected.");
581 readMessage(reader.readLine());
584 if (handler != null) {
585 handler.bridgeOnline();
589 if (handler != null) {
590 handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined");
596 * Event on incoming Bistabiel/Timer/Mono/Interval updates
601 private void updateBistabiel(Integer id, Integer state) {
602 QbusBistabiel qBistabiel = this.bistabiel.get(id);
604 if (qBistabiel != null) {
605 qBistabiel.updateState(state);
607 logger.trace("Bistabiel in controller not known {}", id);
612 * Event on incoming Dimmer updates
617 private void updateDimmer(Integer id, Integer state) {
618 QbusDimmer qDimmer = this.dimmer.get(id);
620 if (qDimmer != null) {
621 qDimmer.updateState(state);
623 logger.trace("Dimmer in controller not known {}", id);
628 * Event on incoming thermostat updates
635 private void updateThermostat(Integer id, int mode, double sp, double ct) {
636 QbusThermostat qThermostat = this.thermostat.get(id);
638 if (qThermostat != null) {
639 qThermostat.updateState(ct, sp, mode);
641 logger.trace("Thermostat in controller not known {}", id);
646 * Event on incoming CO2 updates
651 private void updateCO2(Integer id, Integer state) {
652 QbusCO2 qCO2 = this.co2.get(id);
655 qCO2.updateState(state);
657 logger.trace("CO2 in controller not known {}", id);
662 * Event on incoming screen updates
667 private void updateRol(Integer id, Integer state) {
668 QbusRol qRol = this.rol.get(id);
671 qRol.updateState(state);
673 logger.trace("ROL02P in controller not known {}", id);
678 * Event on incoming screen with slats updates
684 private void updateRolSlats(Integer id, Integer state, Integer slats) {
685 QbusRol qRol = this.rol.get(id);
688 qRol.updateState(state);
689 qRol.updateSlats(slats);
691 logger.trace("ROL02P with slats in controller not known {}", id);
696 * Put Bridge offline when there is no connection from the QbusClient
699 private void eventDisconnect() {
700 QbusBridgeHandler handler = bridgeCallBack;
702 if (handler != null) {
703 handler.bridgePending("Waiting for CTD connection");
708 * Return all Bistabiel/Timers/Mono/Intervals in the Qbus Controller.
712 public Map<Integer, QbusBistabiel> getBistabiel() {
713 return this.bistabiel;
717 * Return all Scenes in the Qbus Controller
721 public Map<Integer, QbusScene> getScene() {
726 * Return all Dimmers outputs in the Qbus Controller.
730 public Map<Integer, QbusDimmer> getDimmer() {
735 * Return all rollershutter/screen outputs in the Qbus Controller.
739 public Map<Integer, QbusRol> getRol() {
744 * Return all Thermostats outputs in the Qbus Controller.
748 public Map<Integer, QbusThermostat> getThermostat() {
749 return this.thermostat;
753 * Return all CO2 outputs in the Qbus Controller.
757 public Map<Integer, QbusCO2> getCo2() {
762 * Method to check if communication with Qbus Server is active
764 * @return True if active
766 public boolean communicationActive() {
767 return qSocket != null;
771 * Method to check if communication with Qbus Client is active
773 * @return True if active
775 public boolean clientConnected() {
780 * @param bridgeCallBack the bridgeCallBack to set
782 public void setBridgeCallBack(QbusBridgeHandler bridgeCallBack) {
783 this.bridgeCallBack = bridgeCallBack;
787 * Get the serial number of the CTD as configured in the Bridge.
789 * @return serial number of controller
791 public @Nullable String getSN() {
796 * Sets the serial number of the CTD, as configured in the Bridge.
798 public void setSN() {
799 QbusBridgeHandler qBridgeHandler = bridgeCallBack;
801 if (qBridgeHandler != null) {
802 this.ctd = qBridgeHandler.getSn();
807 public void handleCommand(ChannelUID channelUID, Command command) {