]> git.basschouten.com Git - openhab-addons.git/blob
4268c42dc15bd2cbce288aa056400c5ad65c8d33
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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             handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Please set a correct port.");
126             return;
127         }
128
129         if (addr == null) {
130             handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Please set the hostname of the Qbus server.");
131             return;
132         }
133
134         Socket socket = null;
135
136         try {
137             socket = new Socket(addr, port);
138             qSocket = socket;
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);
144             return;
145         }
146
147         setSN();
148         getSN();
149
150         // Connect to Qbus server
151         connect();
152
153         // Then start thread to listen to incoming updates from Qbus.
154         threadExecutor.execute(() -> {
155             try {
156                 qbusListener();
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);
163             }
164         });
165
166         if (!ctdConnected) {
167             handler.bridgePending("Waiting for CTD to come online...");
168         }
169     }
170
171     /**
172      * Cleanup socket when the communication with Qbus Server is closed.
173      *
174      * @throws IOException
175      *
176      */
177     public synchronized void stopCommunication() throws IOException {
178         listenerStopped = true;
179
180         Socket socket = qSocket;
181
182         if (socket != null) {
183             try {
184                 socket.close();
185             } catch (IOException ignore) {
186                 // ignore IO Error when trying to close the socket if the intention is to close it anyway
187             }
188         }
189
190         BufferedReader reader = this.qIn;
191         if (reader != null) {
192             reader.close();
193         }
194
195         PrintWriter writer = this.qOut;
196         if (writer != null) {
197             writer.close();
198         }
199
200         qSocket = null;
201         qbusListenerRunning = false;
202         ctdConnected = false;
203
204         logger.trace("Communication stopped from thread {}", Thread.currentThread().getId());
205     }
206
207     /**
208      * Close and restart communication with Qbus Server.
209      *
210      * @throws InterruptedException
211      * @throws IOException
212      */
213     public synchronized void restartCommunication() throws InterruptedException, IOException {
214         stopCommunication();
215
216         startCommunication();
217     }
218
219     /**
220      * Thread that handles incoming messages from Qbus client.
221      * <p>
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.
225      *
226      * @return
227      * @throws IOException
228      * @throws InterruptedException
229      *
230      *
231      */
232     private void qbusListener() throws IOException, InterruptedException {
233         String qMessage;
234
235         listenerStopped = false;
236         qbusListenerRunning = true;
237
238         BufferedReader reader = this.qIn;
239
240         if (reader == null) {
241             throw new IOException("Bufferreader for incoming messages not initialized.");
242         }
243
244         try {
245             while (!Thread.currentThread().isInterrupted() && ((qMessage = reader.readLine()) != null)) {
246                 readMessage(qMessage);
247
248             }
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();
254                 return;
255             }
256         } finally {
257             qbusListenerRunning = false;
258         }
259
260         if (!listenerStopped) {
261             qbusListenerRunning = false;
262
263             QbusBridgeHandler handler = bridgeCallBack;
264
265             if (handler != null) {
266                 ctdConnected = false;
267                 handler.bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, "No communication with Qbus server");
268             }
269         }
270
271         qbusListenerRunning = false;
272         logger.trace("Event listener thread stopped on thread {}", Thread.currentThread().getId());
273     }
274
275     /**
276      * Called by other methods to send json data to Qbus.
277      *
278      * @param qMessage
279      * @throws InterruptedException
280      * @throws IOException
281      */
282     synchronized void sendMessage(Object qMessage) throws InterruptedException, IOException {
283         PrintWriter writer = qOut;
284         String json = gsonOut.toJson(qMessage);
285
286         if (writer != null) {
287             writer.println(json);
288             // Delay after sending data to improve scene execution
289             TimeUnit.MILLISECONDS.sleep(250);
290         }
291
292         if ((writer == null) || (writer.checkError())) {
293             logger.warn("Error sending message, trying to restart communication");
294
295             restartCommunication();
296
297             // retry sending after restart
298             writer = qOut;
299             if (writer != null) {
300                 writer.println(json);
301             }
302             if ((writer == null) || (writer.checkError())) {
303                 logger.warn("Error resending message");
304
305             }
306         }
307     }
308
309     /**
310      * Method that interprets all feedback from Qbus Server application and calls appropriate handling methods.
311      * <ul>
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
318      * </ul>
319      *
320      * @param qMessage message read from Qbus.
321      * @throws InterruptedException
322      * @throws IOException
323      *
324      */
325     private void readMessage(String qMessage) {
326         String sn = null;
327         String cmd = "";
328         String ctd = null;
329         Integer id = null;
330         Integer state = null;
331         Integer mode = null;
332         Double setpoint = null;
333         Double measured = null;
334         Integer slats = null;
335
336         QbusMessageBase qMessageGson;
337         try {
338             qMessageGson = gsonIn.fromJson(qMessage, QbusMessageBase.class);
339
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();
349             }
350         } catch (JsonParseException e) {
351             String msg = e.getMessage();
352             logger.trace("Not acted on unsupported json {} : {}", qMessage, msg);
353             return;
354         }
355         QbusBridgeHandler handler = bridgeCallBack;
356
357         if (handler != null) {
358             sn = handler.getSn();
359         }
360
361         if (sn != null && ctd != null) {
362             try {
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();
367
368                         for (Map<String, String> ctdOutputs : outputs) {
369
370                             String ctdType = ctdOutputs.get("type");
371                             String ctdIdStr = ctdOutputs.get("id");
372                             Integer ctdId = null;
373
374                             if (ctdIdStr != null) {
375                                 ctdId = Integer.parseInt(ctdIdStr);
376                             } else {
377                                 return;
378                             }
379
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");
386
387                                 Integer ctdStateI = null;
388                                 if (ctdState != null) {
389                                     ctdStateI = Integer.parseInt(ctdState);
390                                 }
391
392                                 Integer ctdSlatsI = null;
393                                 if (ctdSlats != null) {
394                                     ctdSlatsI = Integer.parseInt(ctdSlats);
395                                 }
396
397                                 Integer ctdMmodeI = null;
398                                 if (ctdMmode != null) {
399                                     ctdMmodeI = Integer.parseInt(ctdMmode);
400                                 }
401
402                                 Double ctdSetpointD = null;
403                                 if (ctdSetpoint != null) {
404                                     ctdSetpointD = Double.parseDouble(ctdSetpoint);
405                                 }
406
407                                 Double ctdMeasuredD = null;
408                                 if (ctdMeasured != null) {
409                                     ctdMeasuredD = Double.parseDouble(ctdMeasured);
410                                 }
411
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);
419                                         } else {
420                                             output.updateState(ctdStateI);
421                                         }
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);
428                                         } else {
429                                             output.updateState(ctdStateI);
430                                         }
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);
436                                         } else {
437                                             output.updateState(ctdStateI);
438                                         }
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);
444                                         }
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);
452                                             }
453                                             rol.put(ctdId, output);
454                                         } else {
455                                             output.updateState(ctdStateI);
456                                             if (ctdSlats != null) {
457                                                 output.updateSlats(ctdSlatsI);
458                                             }
459                                         }
460                                     }
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);
468                                         } else {
469                                             output.updateState(ctdMeasuredD, ctdSetpointD, ctdMmodeI);
470                                         }
471                                     }
472                                 }
473                             }
474                         }
475                         // Handle update commands from Qbus
476                     } else if ("updateBistabiel".equals(cmd)) {
477                         if (id != null && state != null) {
478                             updateBistabiel(id, state);
479                         }
480                     } else if ("updateDimmer".equals(cmd)) {
481                         if (id != null && state != null) {
482                             updateDimmer(id, state);
483                         }
484                     } else if ("updateDimmer".equals(cmd)) {
485                         if (id != null && state != null) {
486                             updateDimmer(id, state);
487                         }
488                     } else if ("updateCo2".equals(cmd)) {
489                         if (id != null && state != null) {
490                             updateCO2(id, state);
491                         }
492                     } else if ("updateThermostat".equals(cmd)) {
493                         if (id != null && measured != null && setpoint != null && mode != null) {
494                             updateThermostat(id, mode, setpoint, measured);
495                         }
496                     } else if ("updateRol02p".equals(cmd)) {
497                         if (id != null && state != null) {
498                             updateRol(id, state);
499                         }
500                     } else if ("updateRol02pSlat".equals(cmd)) {
501                         if (id != null && state != null && slats != null) {
502                             updateRolSlats(id, state, slats);
503                         }
504                         // Incomming commands from Qbus server to verify the client connection
505                     } else if ("noconnection".equals(cmd)) {
506                         eventDisconnect();
507                     } else if ("connected".equals(cmd)) {
508                         // threadExecutor.execute(() -> {
509                         try {
510                             requestOutputs();
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);
517                         }
518                     }
519                 }
520             } catch (JsonParseException e) {
521                 String msg = e.getMessage();
522                 logger.warn("Not acted on unsupported json {}, {}", qMessage, msg);
523             }
524         }
525     }
526
527     /**
528      * Initialize the communication object
529      */
530     @Override
531     public void initialize() {
532     }
533
534     /**
535      * Initial connection to Qbus Server to open a communication channel
536      *
537      * @throws InterruptedException
538      * @throws IOException
539      */
540     private void connect() throws InterruptedException, IOException {
541         String snr = getSN();
542
543         if (snr != null) {
544             QbusMessageCmd qCmd = new QbusMessageCmd(snr, "openHAB");
545
546             sendMessage(qCmd);
547
548             BufferedReader reader = qIn;
549
550             if (reader == null) {
551                 throw new IOException("Cannot read from socket, reader not connected.");
552             }
553             readMessage(reader.readLine());
554
555         } else {
556             QbusBridgeHandler handler = bridgeCallBack;
557             if (handler != null) {
558                 handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined");
559             }
560         }
561     }
562
563     /**
564      * Send a request for all available outputs and initializes them via readMessage
565      *
566      * @throws InterruptedException
567      * @throws IOException
568      */
569     private void requestOutputs() throws InterruptedException, IOException {
570         String snr = getSN();
571         QbusBridgeHandler handler = bridgeCallBack;
572
573         if (snr != null) {
574             QbusMessageCmd qCmd = new QbusMessageCmd(snr, "all");
575             sendMessage(qCmd);
576
577             BufferedReader reader = qIn;
578             if (reader == null) {
579                 throw new IOException("Cannot read from socket, reader not connected.");
580             }
581             readMessage(reader.readLine());
582             ctdConnected = true;
583
584             if (handler != null) {
585                 handler.bridgeOnline();
586             }
587
588         } else {
589             if (handler != null) {
590                 handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined");
591             }
592         }
593     }
594
595     /**
596      * Event on incoming Bistabiel/Timer/Mono/Interval updates
597      *
598      * @param id
599      * @param state
600      */
601     private void updateBistabiel(Integer id, Integer state) {
602         QbusBistabiel qBistabiel = this.bistabiel.get(id);
603
604         if (qBistabiel != null) {
605             qBistabiel.updateState(state);
606         } else {
607             logger.trace("Bistabiel in controller not known {}", id);
608         }
609     }
610
611     /**
612      * Event on incoming Dimmer updates
613      *
614      * @param id
615      * @param state
616      */
617     private void updateDimmer(Integer id, Integer state) {
618         QbusDimmer qDimmer = this.dimmer.get(id);
619
620         if (qDimmer != null) {
621             qDimmer.updateState(state);
622         } else {
623             logger.trace("Dimmer in controller not known {}", id);
624         }
625     }
626
627     /**
628      * Event on incoming thermostat updates
629      *
630      * @param id
631      * @param mode
632      * @param sp
633      * @param ct
634      */
635     private void updateThermostat(Integer id, int mode, double sp, double ct) {
636         QbusThermostat qThermostat = this.thermostat.get(id);
637
638         if (qThermostat != null) {
639             qThermostat.updateState(ct, sp, mode);
640         } else {
641             logger.trace("Thermostat in controller not known {}", id);
642         }
643     }
644
645     /**
646      * Event on incoming CO2 updates
647      *
648      * @param id
649      * @param state
650      */
651     private void updateCO2(Integer id, Integer state) {
652         QbusCO2 qCO2 = this.co2.get(id);
653
654         if (qCO2 != null) {
655             qCO2.updateState(state);
656         } else {
657             logger.trace("CO2 in controller not known {}", id);
658         }
659     }
660
661     /**
662      * Event on incoming screen updates
663      *
664      * @param id
665      * @param state
666      */
667     private void updateRol(Integer id, Integer state) {
668         QbusRol qRol = this.rol.get(id);
669
670         if (qRol != null) {
671             qRol.updateState(state);
672         } else {
673             logger.trace("ROL02P in controller not known {}", id);
674         }
675     }
676
677     /**
678      * Event on incoming screen with slats updates
679      *
680      * @param id
681      * @param state
682      * @param slats
683      */
684     private void updateRolSlats(Integer id, Integer state, Integer slats) {
685         QbusRol qRol = this.rol.get(id);
686
687         if (qRol != null) {
688             qRol.updateState(state);
689             qRol.updateSlats(slats);
690         } else {
691             logger.trace("ROL02P with slats in controller not known {}", id);
692         }
693     }
694
695     /**
696      * Put Bridge offline when there is no connection from the QbusClient
697      *
698      */
699     private void eventDisconnect() {
700         QbusBridgeHandler handler = bridgeCallBack;
701
702         if (handler != null) {
703             handler.bridgePending("Waiting for CTD connection");
704         }
705     }
706
707     /**
708      * Return all Bistabiel/Timers/Mono/Intervals in the Qbus Controller.
709      *
710      * @return
711      */
712     public Map<Integer, QbusBistabiel> getBistabiel() {
713         return this.bistabiel;
714     }
715
716     /**
717      * Return all Scenes in the Qbus Controller
718      *
719      * @return
720      */
721     public Map<Integer, QbusScene> getScene() {
722         return this.scene;
723     }
724
725     /**
726      * Return all Dimmers outputs in the Qbus Controller.
727      *
728      * @return
729      */
730     public Map<Integer, QbusDimmer> getDimmer() {
731         return this.dimmer;
732     }
733
734     /**
735      * Return all rollershutter/screen outputs in the Qbus Controller.
736      *
737      * @return
738      */
739     public Map<Integer, QbusRol> getRol() {
740         return this.rol;
741     }
742
743     /**
744      * Return all Thermostats outputs in the Qbus Controller.
745      *
746      * @return
747      */
748     public Map<Integer, QbusThermostat> getThermostat() {
749         return this.thermostat;
750     }
751
752     /**
753      * Return all CO2 outputs in the Qbus Controller.
754      *
755      * @return
756      */
757     public Map<Integer, QbusCO2> getCo2() {
758         return this.co2;
759     }
760
761     /**
762      * Method to check if communication with Qbus Server is active
763      *
764      * @return True if active
765      */
766     public boolean communicationActive() {
767         return qSocket != null;
768     }
769
770     /**
771      * Method to check if communication with Qbus Client is active
772      *
773      * @return True if active
774      */
775     public boolean clientConnected() {
776         return ctdConnected;
777     }
778
779     /**
780      * @param bridgeCallBack the bridgeCallBack to set
781      */
782     public void setBridgeCallBack(QbusBridgeHandler bridgeCallBack) {
783         this.bridgeCallBack = bridgeCallBack;
784     }
785
786     /**
787      * Get the serial number of the CTD as configured in the Bridge.
788      *
789      * @return serial number of controller
790      */
791     public @Nullable String getSN() {
792         return this.ctd;
793     }
794
795     /**
796      * Sets the serial number of the CTD, as configured in the Bridge.
797      */
798     public void setSN() {
799         QbusBridgeHandler qBridgeHandler = bridgeCallBack;
800
801         if (qBridgeHandler != null) {
802             this.ctd = qBridgeHandler.getSn();
803         }
804     }
805
806     @Override
807     public void handleCommand(ChannelUID channelUID, Command command) {
808     }
809 }