]> git.basschouten.com Git - openhab-addons.git/commitdiff
[Qbus] Initial contribution (#9191)
authorKoen Schockaert <54985218+QbusKoen@users.noreply.github.com>
Sun, 11 Apr 2021 17:22:37 +0000 (19:22 +0200)
committerGitHub <noreply@github.com>
Sun, 11 Apr 2021 17:22:37 +0000 (19:22 +0200)
Signed-off-by: Koen Schockaert <ks@qbus.be>
34 files changed:
CODEOWNERS
bundles/org.openhab.binding.qbus/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.qbus/README.md [new file with mode: 0644]
bundles/org.openhab.binding.qbus/doc/Logo.JPG [new file with mode: 0644]
bundles/org.openhab.binding.qbus/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties [new file with mode: 0644]
bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index 2af5cc0ce16db5432b3e52fbdf5a718cec5f3be2..24406442bff4e4f6eca3e8b2e9d92e02256c089e 100644 (file)
 /bundles/org.openhab.binding.pulseaudio/ @peuter
 /bundles/org.openhab.binding.pushbullet/ @hakan42
 /bundles/org.openhab.binding.pushover/ @cweitkamp
+/bundles/org.openhab.binding.qbus/ @QbusKoen
 /bundles/org.openhab.binding.radiothermostat/ @mlobstein
 /bundles/org.openhab.binding.regoheatpump/ @crnjan
 /bundles/org.openhab.binding.revogi/ @andibraeu
diff --git a/bundles/org.openhab.binding.qbus/NOTICE b/bundles/org.openhab.binding.qbus/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.qbus/README.md b/bundles/org.openhab.binding.qbus/README.md
new file mode 100644 (file)
index 0000000..50b5bed
--- /dev/null
@@ -0,0 +1,110 @@
+# Qbus Binding
+
+![Qbus Logo](doc/Logo.JPG)
+
+This binding for [Qbus](https://qbus.be) communicates with all controllers of the Qbus home automation system.
+
+We also host a site which contains a [manual](https://manualoh.schockaert.tk/) where you can find lots of information to set up openHAB with Qbus client and server (for the moment only in Dutch).
+
+The controllers can not communicate directly with openHAB, therefore we developed a client/server application which you must install prior to enable this binding.
+More information can be found here:
+[Qbus Client/Server](https://github.com/QbusKoen/QbusClientServer-Installer)
+
+With this binding you can control and read almost every output from the Qbus system.
+
+## Supported Things
+
+The following things are supported by the Qbus binding:
+
+- `dimmer`: Dimmer 1 button, 2 button and clc
+- `onOff`: Bistabiel, Timer1-3, Interval
+- `thermostats`: Thermostats - normal and PID
+- `scene`: Scenes
+- `co2`: CO2 
+- `rollershutter`: Rollershutter 
+- `rollershutter_slats`: Rollerhutter with slats
+
+For now the following Qbus things are not yet supported but will come:
+
+- DMX
+- Timer 4 & 5
+- HVAC
+- Humidity
+- Renson
+- Duco
+- Kinetura
+- Energy monitor
+- Weather station
+
+
+## Discovery
+
+The discovery service is not yet implemented but the System Manager III software of Qbus generates things and item files from the programming, which you can use directly in openHAB.
+
+## Bridge configuration
+
+```
+Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, serverCheck=10 ] {
+...
+}
+```
+
+
+
+| Property      | Default   | Required | Description                                                                                                                          |
+| ------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
+| `addr`        | localhost | YES      | The ip address of the machine where the Qbus Server runs                                                                             |
+| `sn`          |           | YES      | The serial number of your controller                                                                                                 |
+| `port`        | 8447      | YES      | The communication port of the client/server                                                                                          |
+| `serverCheck` | 10     | NO       | Refresh time - After x minutes there will be a check if server is still running and if client is still connected. If not - reconnect |
+
+
+## Things configuration
+
+| Thing Type ID         | Channel Name  | Read only | description                                            |
+| --------------------- | ------------- | --------- | ------------------------------------------------------ |
+| `onOff`               | switch        | No        | This is the channel for Bistable, Timers and Intervals |
+| `dimmer`              | brightness    | No        | This is the channel for Dimmers 1&2 buttons and CLC    |
+| `scene`               | Switch        | No        | This is the channel for scenes                         |
+| `co2`                 | co2           | Yes       | This is the channel for CO2 sensors                    |
+| `rollershutter`       | rollershutter | No        | This is the channel for rollershutters                 |
+| `rollershutter_slats` | rollershutter | No        | This is the channel for rollershutters with slats      |
+| `thermostat`          | setpoint      | No        | This is the channel for thermostats setpoint           |
+| `thermostat`          | measured      | Yes       | This is the channel for thermostats currenttemp        |
+| `thermostat`          | mode          | No        | This is the channel for thermostats mode               |
+
+
+## Full Example
+
+### Things
+
+```
+Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, serverCheck=10 ] {
+    dimmer                   1     "ToonzaalLED"      [ dimmerId=100 ]
+    onOff                    30    "Toonzaal230V"     [ bistabielId=76 ]
+    thermostat               50    "Service"          [ thermostatId=99 ]
+    scene                    70    "Disco"            [ sceneId=36 ]
+    co2                      100   "Productie"        [ co2Id=26 ]
+    rollershutter            120    "Roller1"         [ rolId=268 ]
+    rollershutter_slats      121    "Roller2"         [ rolId=264 ]
+}
+```
+
+### Items
+
+```
+Dimmer              ToonzaalLED          <light>    [ "Lighting" ]      {channel="qbus:dimmer:CTD007841:1:brightness"}
+Switch              Toonzaal230V         <light>                        {channel="qbus:onOff:CTD007841:30:switch"}
+Number:Temperature  ServiceSP"[%.1f %unit%]" (GroepThermostaten)            {channel="qbus:thermostat:CTD007841:50:setpoint"}
+Number:Temperature  ServiceCT"[%.1f %unit%]" (GroepThermostaten)            {channel="qbus:thermostat:CTD007841:50:measured"}
+Number              ServiceMode          (GroepThermostaten)            {channel="qbus:thermostat:CTD007841:50:mode",ihc="0x33c311" , autoupdate="true"}
+Switch              Disco                <light>                        {channel="qbus:scene:CTD007841:36:scene"}
+Number              ProductieCO2                                        {channel="qbus:co2:CTD007841:100:co2"}
+Rollershutter       Roller1                                             {channel="qbus:rollershutter:CTD007841:120:rollershutter"}
+Rollershutter       Roller2                                             {channel="qbus:rollershutter_slats:CTD007841:121:rollershutter"}
+Dimmer              Roller2_slats                                       {channel="qbus:rollershutter_slats:CTD007841:121:slats"}
+```
+
+This is the link to the [Qbus forum](https://qbusforum.be). This forum is mainly in dutch and you can find a lot of information about the pre testings of this binding and offers a way to communicate with other users.
+
diff --git a/bundles/org.openhab.binding.qbus/doc/Logo.JPG b/bundles/org.openhab.binding.qbus/doc/Logo.JPG
new file mode 100644 (file)
index 0000000..93f10e4
Binary files /dev/null and b/bundles/org.openhab.binding.qbus/doc/Logo.JPG differ
diff --git a/bundles/org.openhab.binding.qbus/pom.xml b/bundles/org.openhab.binding.qbus/pom.xml
new file mode 100644 (file)
index 0000000..c611bfc
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.qbus</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Qbus Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml b/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..fcae4ee
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+       Copyright (c) 2010-2020 Contributors to the openHAB project
+
+       See the NOTICE file(s) distributed with this work for additional
+       information.
+
+       This program and the accompanying materials are made available under the
+       terms of the Eclipse Public License 2.0 which is available at
+       http://www.eclipse.org/legal/epl-2.0
+
+       SPDX-License-Identifier: EPL-2.0
+
+-->
+<features name="org.openhab.binding.qbus-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-qbus" description="Qbus Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.qbus/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java
new file mode 100644 (file)
index 0000000..57b0cf5
--- /dev/null
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link QbusBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Koen Schockaert - Initial contribution
+ */
+@NonNullByDefault
+public class QbusBindingConstants {
+
+    private static final String BINDING_ID = "qbus";
+
+    // bridge
+    public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "bridge");
+    public static final Set<ThingTypeUID> BRIDGE_THING_TYPES_UIDS = Collections.singleton(BRIDGE_THING_TYPE);
+    // Bridge config properties
+    public static final String CONFIG_HOST_NAME = "addr";
+    public static final String CONFIG_PORT = "port";
+    public static final String CONFIG_SN = "sn";
+    public static final String CONFIG_SERVERCHECK = "serverCheck";
+
+    // generic thing types
+    public static final ThingTypeUID THING_TYPE_CO2 = new ThingTypeUID(BINDING_ID, "co2");
+    public static final ThingTypeUID THING_TYPE_SCENE = new ThingTypeUID(BINDING_ID, "scene");
+    public static final ThingTypeUID THING_TYPE_ON_OFF_LIGHT = new ThingTypeUID(BINDING_ID, "onOff");
+    public static final ThingTypeUID THING_TYPE_DIMMABLE_LIGHT = new ThingTypeUID(BINDING_ID, "dimmer");
+    public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER = new ThingTypeUID(BINDING_ID, "rollershutter");
+    public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER_SLATS = new ThingTypeUID(BINDING_ID,
+            "rollershutter_slats");
+    public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat");
+
+    // List of all Thing Type UIDs
+    public static final Set<ThingTypeUID> SCENE_THING_TYPES_UIDS = Set.of(THING_TYPE_SCENE);
+    public static final Set<ThingTypeUID> CO2_THING_TYPES_UIDS = Set.of(THING_TYPE_CO2);
+    public static final Set<ThingTypeUID> ROLLERSHUTTER_THING_TYPES_UIDS = Set.of(THING_TYPE_ROLLERSHUTTER);
+    public static final Set<ThingTypeUID> ROLLERSHUTTER_SLATS_THING_TYPES_UIDS = Set.of(THING_TYPE_ROLLERSHUTTER_SLATS);
+    public static final Set<ThingTypeUID> BISTABIEL_THING_TYPES_UIDS = Set.of(THING_TYPE_ON_OFF_LIGHT);
+    public static final Set<ThingTypeUID> THERMOSTAT_THING_TYPES_UIDS = Set.of(THING_TYPE_THERMOSTAT);
+    public static final Set<ThingTypeUID> DIMMER_THING_TYPES_UIDS = Set.of(THING_TYPE_ON_OFF_LIGHT,
+            THING_TYPE_DIMMABLE_LIGHT);
+
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ON_OFF_LIGHT,
+            THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_THERMOSTAT, THING_TYPE_SCENE, THING_TYPE_CO2,
+            THING_TYPE_ROLLERSHUTTER, THING_TYPE_ROLLERSHUTTER_SLATS);
+
+    // List of all Channel ids
+    public static final String CHANNEL_SWITCH = "switch";
+    public static final String CHANNEL_SCENE = "scene";
+    public static final String CHANNEL_BRIGHTNESS = "brightness";
+    public static final String CHANNEL_MEASURED = "measured";
+    public static final String CHANNEL_SETPOINT = "setpoint";
+    public static final String CHANNEL_MODE = "mode";
+    public static final String CHANNEL_CO2 = "co2";
+    public static final String CHANNEL_ROLLERSHUTTER = "rollershutter";
+    public static final String CHANNEL_SLATS = "slats";
+
+    // Thing config properties
+    public static final String CONFIG_BISTABIEL_ID = "bistabielId";
+    public static final String CONFIG_DIMMER_ID = "dimmerId";
+    public static final String CONFIG_THERMOSTAT_ID = "thermostatId";
+    public static final String CONFIG_SCENE_ID = "sceneId";
+    public static final String CONFIG_CO2_ID = "co2Id";
+    public static final String CONFIG_ROLLERSHUTTER_ID = "rolId";
+    public static final String CONFIG_STEP_VALUE = "step";
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java
new file mode 100644 (file)
index 0000000..2f62566
--- /dev/null
@@ -0,0 +1,359 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.qbus.internal;
+
+import java.io.IOException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.protocol.QbusCommunication;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link QbusBridgeHandler} is the handler for a Qbus controller
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public class QbusBridgeHandler extends BaseBridgeHandler {
+
+    private @Nullable QbusCommunication qbusComm;
+
+    protected @Nullable QbusConfiguration bridgeConfig = new QbusConfiguration();
+
+    private @Nullable ScheduledFuture<?> refreshTimer;
+
+    private final Logger logger = LoggerFactory.getLogger(QbusBridgeHandler.class);
+
+    public QbusBridgeHandler(Bridge Bridge) {
+        super(Bridge);
+    }
+
+    /**
+     * Initialize the bridge
+     */
+    @Override
+    public void initialize() {
+        Integer serverCheck = getServerCheck();
+
+        readConfig();
+
+        createCommunicationObject();
+
+        if (serverCheck != null) {
+            this.setupRefreshTimer(serverCheck);
+        }
+    }
+
+    /**
+     * Sets the Bridge call back
+     */
+    private void setBridgeCallBack() {
+        QbusCommunication qbusCommunication = getQbusCommunication();
+        if (qbusCommunication != null) {
+            qbusCommunication.setBridgeCallBack(this);
+        }
+    }
+
+    /**
+     * Create communication object to Qbus server and start communication.
+     *
+     * @param addr : IP address of Qbus server
+     * @param port : Communication port of QbusServer
+     */
+    private void createCommunicationObject() {
+        scheduler.submit(() -> {
+
+            setQbusCommunication(new QbusCommunication(thing));
+
+            QbusCommunication qbusCommunication = getQbusCommunication();
+
+            setBridgeCallBack();
+
+            Integer serverCheck = getServerCheck();
+            String sn = getSn();
+            if (serverCheck != null) {
+                if (sn != null) {
+                    if (qbusCommunication != null) {
+                        try {
+                            qbusCommunication.startCommunication();
+                        } catch (InterruptedException e) {
+                            String msg = e.getMessage();
+                            bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                                    "Communication wit Qbus server could not be established, will try to reconnect every "
+                                            + serverCheck + " minutes. InterruptedException: " + msg);
+                            return;
+                        } catch (IOException e) {
+                            String msg = e.getMessage();
+                            bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                                    "Communication wit Qbus server could not be established, will try to reconnect every "
+                                            + serverCheck + " minutes. IOException: " + msg);
+                            return;
+                        }
+
+                        if (!qbusCommunication.communicationActive()) {
+                            bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                                    "No communication with Qbus Server, will try to reconnect every " + serverCheck
+                                            + " minutes");
+                            return;
+                        }
+
+                        if (!qbusCommunication.clientConnected()) {
+                            bridgePending("Waiting for Qbus client to come online");
+                            return;
+                        }
+
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Updates offline status off the Bridge when an error occurs.
+     *
+     * @param status
+     * @param detail
+     * @param message
+     */
+    public void bridgeOffline(ThingStatusDetail detail, String message) {
+        updateStatus(ThingStatus.OFFLINE, detail, message);
+    }
+
+    /**
+     * Updates pending status off the Bridge (usualay when Qbus client id not connected)
+     *
+     * @param message
+     */
+    public void bridgePending(String message) {
+        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING, message);
+    }
+
+    /**
+     * Put bridge online when error in communication resolved.
+     */
+    public void bridgeOnline() {
+        updateStatus(ThingStatus.ONLINE);
+    }
+
+    /**
+     * Initializes a timer that check the communication with Qbus server/client and tries to re-establish communication.
+     *
+     * @param refreshInterval Time before refresh in minutes.
+     */
+    private void setupRefreshTimer(int refreshInterval) {
+        ScheduledFuture<?> timer = refreshTimer;
+
+        if (timer != null) {
+            timer.cancel(true);
+            refreshTimer = null;
+        }
+
+        if (refreshInterval == 0) {
+            return;
+        }
+
+        refreshTimer = scheduler.scheduleWithFixedDelay(() -> {
+            QbusCommunication comm = getCommunication();
+            Integer serverCheck = getServerCheck();
+
+            if (comm != null) {
+                if (serverCheck != null) {
+                    if (!comm.communicationActive()) {
+                        // Disconnected from Qbus Server, restart communication
+                        try {
+                            comm.startCommunication();
+                        } catch (InterruptedException e) {
+                            String msg = e.getMessage();
+                            bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                                    "Communication wit Qbus server could not be established, will try to reconnect every "
+                                            + serverCheck + " minutes. InterruptedException: " + msg);
+                        } catch (IOException e) {
+                            String msg = e.getMessage();
+                            bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                                    "Communication wit Qbus server could not be established, will try to reconnect every "
+                                            + serverCheck + " minutes. IOException: " + msg);
+                        }
+                    }
+                }
+            }
+        }, refreshInterval, refreshInterval, TimeUnit.MINUTES);
+    }
+
+    /**
+     * Disposes the Bridge and stops communication with the Qbus server
+     */
+    @Override
+    public void dispose() {
+        ScheduledFuture<?> timer = refreshTimer;
+        if (timer != null) {
+            timer.cancel(true);
+        }
+
+        refreshTimer = null;
+
+        QbusCommunication comm = getCommunication();
+
+        if (comm != null) {
+            try {
+                comm.stopCommunication();
+            } catch (IOException e) {
+                String message = e.toString();
+                logger.debug("Error on stopping communication.{} ", message);
+            }
+        }
+
+        comm = null;
+    }
+
+    /**
+     * Reconnect to Qbus server if controller is offline
+     */
+    public void ctdOffline() {
+        bridgePending("Waiting for CTD connection");
+    }
+
+    /**
+     * Get BridgeCommunication
+     *
+     * @return BridgeCommunication
+     */
+    public @Nullable QbusCommunication getQbusCommunication() {
+        if (this.qbusComm != null) {
+            return this.qbusComm;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Sets BridgeCommunication
+     *
+     * @param BridgeCommunication
+     */
+    void setQbusCommunication(QbusCommunication comm) {
+        this.qbusComm = comm;
+    }
+
+    /**
+     * Gets the status off the Bridge
+     *
+     * @return
+     */
+    public ThingStatus getStatus() {
+        return thing.getStatus();
+    }
+
+    /**
+     * Gets the status off the Bridge
+     *
+     * @return
+     */
+    public ThingStatusDetail getStatusDetails() {
+        ThingStatusInfo status = thing.getStatusInfo();
+        ThingStatusDetail detail = status.getStatusDetail();
+        return detail;
+    }
+
+    /**
+     * Sets the configuration parameters
+     */
+    protected void readConfig() {
+        bridgeConfig = getConfig().as(QbusConfiguration.class);
+    }
+
+    /**
+     * Get the Qbus communication object.
+     *
+     * @return Qbus communication object
+     */
+    public @Nullable QbusCommunication getCommunication() {
+        return this.qbusComm;
+    }
+
+    /**
+     * Get the ip address of the Qbus server.
+     *
+     * @return the ip address
+     */
+    public @Nullable String getAddress() {
+        QbusConfiguration localConfig = this.bridgeConfig;
+
+        if (localConfig != null) {
+            return localConfig.addr;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Get the listening port of the Qbus server.
+     *
+     * @return
+     */
+    public @Nullable Integer getPort() {
+        QbusConfiguration localConfig = this.bridgeConfig;
+
+        if (localConfig != null) {
+            return localConfig.port;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Get the serial nr of the Qbus server.
+     *
+     * @return the serial nr of the controller
+     */
+    public @Nullable String getSn() {
+        QbusConfiguration localConfig = this.bridgeConfig;
+
+        if (localConfig != null) {
+            return localConfig.sn;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Get the refresh interval.
+     *
+     * @return the refresh interval
+     */
+    public @Nullable Integer getServerCheck() {
+        QbusConfiguration localConfig = this.bridgeConfig;
+
+        if (localConfig != null) {
+            return localConfig.serverCheck;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java
new file mode 100644 (file)
index 0000000..6a68c94
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.qbus.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Class {@link QbusConfiguration} Configuration Class
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public class QbusConfiguration {
+    public @Nullable String addr;
+    public @Nullable Integer port;
+    public @Nullable String sn;
+    public @Nullable Integer serverCheck;
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java
new file mode 100644 (file)
index 0000000..6de139b
--- /dev/null
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal;
+
+import static org.openhab.binding.qbus.internal.QbusBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler;
+import org.openhab.binding.qbus.internal.handler.QbusCO2Handler;
+import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler;
+import org.openhab.binding.qbus.internal.handler.QbusRolHandler;
+import org.openhab.binding.qbus.internal.handler.QbusSceneHandler;
+import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link qbusHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@Component(service = ThingHandlerFactory.class, configurationPid = "binding.qbus")
+@NonNullByDefault
+public class QbusHandlerFactory extends BaseThingHandlerFactory {
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID) || BRIDGE_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        if (BRIDGE_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) {
+            QbusBridgeHandler handler = new QbusBridgeHandler((Bridge) thing);
+            return handler;
+        } else if (SCENE_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) {
+            return new QbusSceneHandler(thing);
+        } else if (BISTABIEL_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) {
+            return new QbusBistabielHandler(thing);
+        } else if (THERMOSTAT_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) {
+            return new QbusThermostatHandler(thing);
+        } else if (DIMMER_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) {
+            return new QbusDimmerHandler(thing);
+        } else if (CO2_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) {
+            return new QbusCO2Handler(thing);
+        } else if (ROLLERSHUTTER_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) {
+            return new QbusRolHandler(thing);
+        } else if (ROLLERSHUTTER_SLATS_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) {
+            return new QbusRolHandler(thing);
+        }
+
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java
new file mode 100644 (file)
index 0000000..76f0d79
--- /dev/null
@@ -0,0 +1,244 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.handler;
+
+import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_SWITCH;
+import static org.openhab.core.types.RefreshType.REFRESH;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.QbusBridgeHandler;
+import org.openhab.binding.qbus.internal.protocol.QbusBistabiel;
+import org.openhab.binding.qbus.internal.protocol.QbusCommunication;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link QbusBistabielHandler} is responsible for handling the Bistable outputs of Qbus
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public class QbusBistabielHandler extends QbusGlobalHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(QbusBistabielHandler.class);
+
+    protected @Nullable QbusThingsConfig bistabielConfig = new QbusThingsConfig();
+
+    private @Nullable Integer bistabielId;
+
+    private @Nullable String sn;
+
+    public QbusBistabielHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Main initialization
+     */
+    @Override
+    public void initialize() {
+        readConfig();
+
+        this.bistabielId = getId();
+
+        setSN();
+
+        scheduler.submit(() -> {
+            QbusCommunication controllerComm;
+
+            if (this.bistabielId != null) {
+                controllerComm = getCommunication("Bistabiel", this.bistabielId);
+            } else {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for BISTABIEL no set! " + this.bistabielId);
+                return;
+            }
+
+            if (controllerComm == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "ID for BISTABIEL not known in controller " + this.bistabielId);
+                return;
+            }
+
+            Map<Integer, QbusBistabiel> bistabielCommLocal = controllerComm.getBistabiel();
+
+            QbusBistabiel outputLocal = bistabielCommLocal.get(this.bistabielId);
+
+            if (outputLocal == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "Bridge could not initialize BISTABIEL ID " + this.bistabielId);
+                return;
+            }
+
+            outputLocal.setThingHandler(this);
+            handleStateUpdate(outputLocal);
+
+            QbusBridgeHandler qBridgeHandler = getBridgeHandler("Bistabiel", this.bistabielId);
+
+            if (qBridgeHandler != null) {
+                if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) {
+                    updateStatus(ThingStatus.ONLINE);
+                } else {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
+                            "Bridge offline for BISTABIEL ID " + this.bistabielId);
+                }
+            }
+        });
+    }
+
+    /**
+     * Handle the status update from the bistabiel
+     */
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        QbusCommunication qComm = getCommunication("Bistabiel", this.bistabielId);
+
+        if (qComm == null) {
+            thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                    "ID for BISTABIEL not known in controller " + this.bistabielId);
+            return;
+        } else {
+            Map<Integer, QbusBistabiel> bistabielComm = qComm.getBistabiel();
+
+            QbusBistabiel qBistabiel = bistabielComm.get(this.bistabielId);
+
+            if (qBistabiel == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "ID for BISTABIEL not known in controller " + this.bistabielId);
+                return;
+            } else {
+                scheduler.submit(() -> {
+                    if (!qComm.communicationActive()) {
+                        restartCommunication(qComm, "Bistabiel", this.bistabielId);
+                    }
+
+                    if (qComm.communicationActive()) {
+                        if (command == REFRESH) {
+                            handleStateUpdate(qBistabiel);
+                            return;
+                        }
+
+                        switch (channelUID.getId()) {
+                            case CHANNEL_SWITCH:
+                                try {
+                                    handleSwitchCommand(qBistabiel, command);
+                                } catch (IOException e) {
+                                    String message = e.getMessage();
+                                    logger.warn("Error on executing Switch for bistabiel ID {}. IOException: {}",
+                                            this.bistabielId, message);
+                                } catch (InterruptedException e) {
+                                    String message = e.getMessage();
+                                    logger.warn(
+                                            "Error on executing Switch for bistabiel ID {}. Interruptedexception {}",
+                                            this.bistabielId, message);
+                                }
+                                break;
+
+                            default:
+                                thingOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                                        "Unknown Channel " + channelUID.getId());
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * Executes the switch command
+     *
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    private void handleSwitchCommand(QbusBistabiel qBistabiel, Command command)
+            throws InterruptedException, IOException {
+        String snr = getSN();
+        if (snr != null) {
+            if (command instanceof OnOffType) {
+                if (command == OnOffType.OFF) {
+                    qBistabiel.execute(0, snr);
+                } else {
+                    qBistabiel.execute(100, snr);
+                }
+            } else {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "No serial number configured for BISTABIEL " + this.bistabielId);
+            }
+        }
+    }
+
+    /**
+     * Method to update state of channel, called from Qbus Bistabiel.
+     *
+     * @param qBistabiel
+     */
+    public void handleStateUpdate(QbusBistabiel qBistabiel) {
+        Integer bistabielState = qBistabiel.getState();
+        if (bistabielState != null) {
+            updateState(CHANNEL_SWITCH, (bistabielState == 0) ? OnOffType.OFF : OnOffType.ON);
+        }
+    }
+
+    /**
+     * Returns the serial number of the controller
+     *
+     * @return the serial nr
+     */
+    public @Nullable String getSN() {
+        return sn;
+    }
+
+    /**
+     * Sets the serial number of the controller
+     */
+    public void setSN() {
+        QbusBridgeHandler qBridgeHandler = getBridgeHandler("Bistabiel", this.bistabielId);
+        if (qBridgeHandler == null) {
+            thingOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                    "No communication with Qbus Bridge for BISTABIEL " + this.bistabielId);
+            return;
+        }
+        sn = qBridgeHandler.getSn();
+    }
+
+    /**
+     * Read the configuration
+     */
+    protected synchronized void readConfig() {
+        bistabielConfig = getConfig().as(QbusThingsConfig.class);
+    }
+
+    /**
+     * Returns the Id from the configuration
+     *
+     * @return outputId
+     */
+    public @Nullable Integer getId() {
+        QbusThingsConfig localConfig = bistabielConfig;
+        if (localConfig != null) {
+            return localConfig.bistabielId;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java
new file mode 100644 (file)
index 0000000..5b191b4
--- /dev/null
@@ -0,0 +1,197 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.handler;
+
+import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_CO2;
+import static org.openhab.core.types.RefreshType.REFRESH;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.QbusBridgeHandler;
+import org.openhab.binding.qbus.internal.protocol.QbusCO2;
+import org.openhab.binding.qbus.internal.protocol.QbusCommunication;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+
+/**
+ * The {@link QbusCO2Handler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public class QbusCO2Handler extends QbusGlobalHandler {
+    protected @Nullable QbusThingsConfig config;
+
+    protected @Nullable QbusThingsConfig co2Config = new QbusThingsConfig();
+
+    private @Nullable Integer co2Id;
+
+    private @Nullable String sn;
+
+    public QbusCO2Handler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Main initialization
+     */
+    @Override
+    public void initialize() {
+        readConfig();
+
+        this.co2Id = getId();
+
+        setSN();
+
+        scheduler.submit(() -> {
+            QbusCommunication controllerComm;
+
+            if (this.co2Id != null) {
+                controllerComm = getCommunication("CO2", this.co2Id);
+            } else {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for CO2 no set! " + this.co2Id);
+                return;
+            }
+
+            if (controllerComm == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for CO2 not known in controller " + this.co2Id);
+                return;
+            }
+
+            Map<Integer, QbusCO2> co2CommLocal = controllerComm.getCo2();
+
+            QbusCO2 outputLocal = co2CommLocal.get(this.co2Id);
+
+            if (outputLocal == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Bridge could not initialize CO2 ID " + this.co2Id);
+                return;
+            }
+
+            outputLocal.setThingHandler(this);
+            handleStateUpdate(outputLocal);
+
+            QbusBridgeHandler qBridgeHandler = getBridgeHandler("CO2", this.co2Id);
+
+            if (qBridgeHandler != null) {
+                if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) {
+                    updateStatus(ThingStatus.ONLINE);
+                } else {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
+                            "Bridge offline for CO2 ID " + this.co2Id);
+                }
+            }
+        });
+    }
+
+    /**
+     * Handle the status update from the thing
+     */
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        QbusCommunication qComm = getCommunication("CO2", this.co2Id);
+
+        if (qComm == null) {
+            thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for CO2 not known in controller " + this.co2Id);
+            return;
+        } else {
+            Map<Integer, QbusCO2> co2Comm = qComm.getCo2();
+
+            QbusCO2 qCo2 = co2Comm.get(this.co2Id);
+
+            if (qCo2 == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for CO2 not known in controller " + this.co2Id);
+                return;
+            } else {
+                scheduler.submit(() -> {
+                    if (!qComm.communicationActive()) {
+                        restartCommunication(qComm, "CO2", this.co2Id);
+                    }
+
+                    if (qComm.communicationActive()) {
+                        if (command == REFRESH) {
+                            handleStateUpdate(qCo2);
+                            return;
+                        }
+
+                        switch (channelUID.getId()) {
+                            default:
+                                thingOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                                        "Unknown Channel " + channelUID.getId());
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * Method to update state of channel, called from Qbus CO2.
+     */
+    public void handleStateUpdate(QbusCO2 qCo2) {
+        Integer co2State = qCo2.getState();
+        if (co2State != null) {
+            updateState(CHANNEL_CO2, new DecimalType(co2State));
+        }
+    }
+
+    /**
+     * Returns the serial number of the controller
+     *
+     * @return the serial nr
+     */
+    public @Nullable String getSN() {
+        return sn;
+    }
+
+    /**
+     * Sets the serial number of the controller
+     */
+    public void setSN() {
+        QbusBridgeHandler qBridgeHandler = getBridgeHandler("CO2", this.co2Id);
+        if (qBridgeHandler == null) {
+            thingOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                    "No communication with Qbus Bridge for CO2 " + this.co2Id);
+            return;
+        }
+        sn = qBridgeHandler.getSn();
+    }
+
+    /**
+     * Read the configuration
+     */
+    protected synchronized void readConfig() {
+        co2Config = getConfig().as(QbusThingsConfig.class);
+    }
+
+    /**
+     * Returns the Id from the configuration
+     *
+     * @return outputId
+     */
+    public @Nullable Integer getId() {
+        QbusThingsConfig localConfig = this.co2Config;
+        if (localConfig != null) {
+            return localConfig.co2Id;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java
new file mode 100644 (file)
index 0000000..fc91c6a
--- /dev/null
@@ -0,0 +1,310 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.handler;
+
+import static org.openhab.binding.qbus.internal.QbusBindingConstants.*;
+import static org.openhab.core.types.RefreshType.REFRESH;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.QbusBridgeHandler;
+import org.openhab.binding.qbus.internal.protocol.QbusCommunication;
+import org.openhab.binding.qbus.internal.protocol.QbusDimmer;
+import org.openhab.core.library.types.IncreaseDecreaseType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link QbusDimmerHandler} is responsible for handling the dimmable outputs of Qbus
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public class QbusDimmerHandler extends QbusGlobalHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(QbusDimmerHandler.class);
+
+    protected @Nullable QbusThingsConfig dimmerConfig = new QbusThingsConfig();
+
+    private @Nullable Integer dimmerId;
+
+    private @Nullable String sn;
+
+    public QbusDimmerHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Main initialization
+     */
+    @Override
+    public void initialize() {
+        readConfig();
+
+        this.dimmerId = getId();
+
+        setSN();
+
+        scheduler.submit(() -> {
+            QbusCommunication controllerComm;
+
+            if (this.dimmerId != null) {
+                controllerComm = getCommunication("Dimmer", this.dimmerId);
+            } else {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for DIMMER no set!  " + this.dimmerId);
+                return;
+            }
+
+            if (controllerComm == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "ID for DIMMER not known in controller " + this.dimmerId);
+                return;
+            }
+
+            Map<Integer, QbusDimmer> dimmerCommLocal = controllerComm.getDimmer();
+
+            QbusDimmer outputLocal = dimmerCommLocal.get(this.dimmerId);
+
+            if (outputLocal == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "Bridge could not initialize DIMMER ID " + this.dimmerId);
+                return;
+            }
+
+            outputLocal.setThingHandler(this);
+            handleStateUpdate(outputLocal);
+
+            QbusBridgeHandler qBridgeHandler = getBridgeHandler("Dimmer", this.dimmerId);
+
+            if (qBridgeHandler != null) {
+                if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) {
+                    updateStatus(ThingStatus.ONLINE);
+                } else {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
+                            "Bridge offline for DIMMER ID " + this.dimmerId);
+                }
+            }
+        });
+    }
+
+    /**
+     * Handle the status update from the dimmer
+     */
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        QbusCommunication qComm = getCommunication("Dimmer", this.dimmerId);
+
+        if (qComm == null) {
+            thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                    "ID for DIMMER not known in controller " + this.dimmerId);
+            return;
+        } else {
+            Map<Integer, QbusDimmer> dimmerComm = qComm.getDimmer();
+
+            QbusDimmer qDimmer = dimmerComm.get(this.dimmerId);
+
+            if (qDimmer == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "ID for DIMMER not known in controller " + this.dimmerId);
+                return;
+            } else {
+                scheduler.submit(() -> {
+                    if (!qComm.communicationActive()) {
+                        restartCommunication(qComm, "Dimmer", this.dimmerId);
+                    }
+
+                    if (qComm.communicationActive()) {
+                        if (command == REFRESH) {
+                            handleStateUpdate(qDimmer);
+                            return;
+                        }
+
+                        switch (channelUID.getId()) {
+                            case CHANNEL_SWITCH:
+                                try {
+                                    handleSwitchCommand(qDimmer, command);
+                                } catch (IOException e) {
+                                    String message = e.getMessage();
+                                    logger.warn("Error on executing Switch for dimmer ID {}. IOException: {}",
+                                            this.dimmerId, message);
+                                } catch (InterruptedException e) {
+                                    String message = e.getMessage();
+                                    logger.warn("Error on executing Switch for dimmer ID {}. Interruptedexception {}",
+                                            this.dimmerId, message);
+                                }
+                                break;
+
+                            case CHANNEL_BRIGHTNESS:
+                                try {
+                                    handleBrightnessCommand(qDimmer, command);
+                                } catch (IOException e) {
+                                    String message = e.getMessage();
+                                    logger.warn("Error on executing Brightness for dimmer ID {}. IOException: {}",
+                                            this.dimmerId, message);
+                                } catch (InterruptedException e) {
+                                    String message = e.getMessage();
+                                    logger.warn(
+                                            "Error on executing Brightness for dimmer ID {}. Interruptedexception {}",
+                                            this.dimmerId, message);
+                                }
+                                break;
+
+                            default:
+                                thingOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                                        "Unknown Channel " + channelUID.getId());
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * Executes the switch command
+     *
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    private void handleSwitchCommand(QbusDimmer qDimmer, Command command) throws InterruptedException, IOException {
+        if (command instanceof OnOffType) {
+            String snr = getSN();
+            if (snr != null) {
+                if (command == OnOffType.OFF) {
+                    qDimmer.execute(0, snr);
+                } else {
+                    qDimmer.execute(1000, snr);
+                }
+            } else {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "No serial number configured for DIMMER " + this.dimmerId);
+            }
+        }
+    }
+
+    /**
+     * Executes the brightness command
+     *
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    private void handleBrightnessCommand(QbusDimmer qDimmer, Command command) throws InterruptedException, IOException {
+        String snr = getSN();
+
+        if (snr == null) {
+            thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                    "No serial number configured for DIMMER " + this.dimmerId);
+            return;
+        } else {
+            if (command instanceof OnOffType) {
+                if (command == OnOffType.OFF) {
+                    qDimmer.execute(0, snr);
+                } else {
+                    qDimmer.execute(100, snr);
+                }
+            } else if (command instanceof IncreaseDecreaseType) {
+                int stepValue = ((Number) getConfig().get(CONFIG_STEP_VALUE)).intValue();
+                Integer currentValue = qDimmer.getState();
+                Integer newValue;
+                Integer sendvalue;
+                if (currentValue != null) {
+                    if (command == IncreaseDecreaseType.INCREASE) {
+                        newValue = currentValue + stepValue;
+                        // round down to step multiple
+                        newValue = newValue - newValue % stepValue;
+                        sendvalue = newValue > 100 ? 100 : newValue;
+                        qDimmer.execute(sendvalue, snr);
+                    } else {
+                        newValue = currentValue - stepValue;
+                        // round up to step multiple
+                        newValue = newValue + newValue % stepValue;
+                        sendvalue = newValue < 0 ? 0 : newValue;
+                        qDimmer.execute(sendvalue, snr);
+                    }
+                }
+            } else if (command instanceof PercentType) {
+                int percentToInt = ((PercentType) command).intValue();
+                if (command == PercentType.ZERO) {
+                    qDimmer.execute(0, snr);
+                } else {
+                    qDimmer.execute(percentToInt, snr);
+                }
+            }
+        }
+    }
+
+    /**
+     * Method to update state of channel, called from Qbus Dimmer.
+     *
+     * @param qDimmer
+     */
+    public void handleStateUpdate(QbusDimmer qDimmer) {
+        Integer dimmerState = qDimmer.getState();
+        if (dimmerState != null) {
+            updateState(CHANNEL_BRIGHTNESS, new PercentType(dimmerState));
+        }
+    }
+
+    /**
+     * Returns the serial number of the controller
+     *
+     * @return the serial number
+     */
+    public @Nullable String getSN() {
+        return sn;
+    }
+
+    /**
+     * Sets the serial number of the controller
+     */
+    public void setSN() {
+        QbusBridgeHandler qBridgeHandler = getBridgeHandler("Dimmer", this.dimmerId);
+        if (qBridgeHandler == null) {
+            thingOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                    "No communication with Qbus Bridge for DIMMER " + this.dimmerId);
+            return;
+        }
+        this.sn = qBridgeHandler.getSn();
+    }
+
+    /**
+     * Read the configuration
+     */
+    protected synchronized void readConfig() {
+        dimmerConfig = getConfig().as(QbusThingsConfig.class);
+    }
+
+    /**
+     * Returns the Id from the configuration
+     *
+     * @return outputId
+     */
+    public @Nullable Integer getId() {
+        QbusThingsConfig localConfig = dimmerConfig;
+        if (localConfig != null) {
+            return localConfig.dimmerId;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java
new file mode 100644 (file)
index 0000000..068813c
--- /dev/null
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.handler;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.QbusBridgeHandler;
+import org.openhab.binding.qbus.internal.protocol.QbusCommunication;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+
+/**
+ * The {@link QbusGlobalHandler} is used in other handlers, to share the functions.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public abstract class QbusGlobalHandler extends BaseThingHandler {
+
+    public QbusGlobalHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Get Bridge communication
+     *
+     * @param type
+     * @param globalId
+     * @return
+     */
+    public @Nullable QbusCommunication getCommunication(String type, @Nullable Integer globalId) {
+        QbusBridgeHandler qBridgeHandler = null;
+        if (globalId != null) {
+            qBridgeHandler = getBridgeHandler(type, globalId);
+        }
+
+        if (qBridgeHandler == null) {
+            updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED,
+                    "No bridge handler initialized for " + type + " with id " + globalId + ".");
+            return null;
+        }
+        QbusCommunication qComm = qBridgeHandler.getCommunication();
+        return qComm;
+    }
+
+    /**
+     * Get the Bridge handler
+     *
+     * @param type
+     * @param globalId
+     * @return
+     */
+    public @Nullable QbusBridgeHandler getBridgeHandler(String type, @Nullable Integer globalId) {
+        Bridge qBridge = getBridge();
+        if (qBridge == null) {
+            updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED,
+                    "No bridge initialized for " + type + " with ID " + globalId);
+            return null;
+        }
+        QbusBridgeHandler qBridgeHandler = (QbusBridgeHandler) qBridge.getHandler();
+        return qBridgeHandler;
+    }
+
+    /**
+     *
+     * @param qComm
+     * @param type
+     * @param globalId
+     */
+    public void restartCommunication(QbusCommunication qComm, String type, @Nullable Integer globalId) {
+        try {
+            qComm.restartCommunication();
+        } catch (InterruptedException e) {
+            String message = e.toString();
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
+        } catch (IOException e) {
+            String message = e.toString();
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
+        }
+
+        QbusBridgeHandler qBridgeHandler = getBridgeHandler(type, globalId);
+
+        if (qBridgeHandler != null && qComm.communicationActive()) {
+            qBridgeHandler.bridgeOnline();
+        } else {
+            thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Communication socket error");
+        }
+    }
+
+    /**
+     * Put thing offline
+     *
+     * @param message
+     */
+    public void thingOffline(ThingStatusDetail detail, String message) {
+        updateStatus(ThingStatus.OFFLINE, detail, message);
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java
new file mode 100644 (file)
index 0000000..0a1a532
--- /dev/null
@@ -0,0 +1,333 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.handler;
+
+import static org.openhab.binding.qbus.internal.QbusBindingConstants.*;
+import static org.openhab.core.library.types.UpDownType.DOWN;
+import static org.openhab.core.types.RefreshType.REFRESH;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.QbusBridgeHandler;
+import org.openhab.binding.qbus.internal.protocol.QbusCommunication;
+import org.openhab.binding.qbus.internal.protocol.QbusRol;
+import org.openhab.core.library.types.IncreaseDecreaseType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.UpDownType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link QbusRolHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public class QbusRolHandler extends QbusGlobalHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(QbusRolHandler.class);
+
+    protected @Nullable QbusThingsConfig rolConfig = new QbusThingsConfig();
+
+    private @Nullable Integer rolId;
+
+    private @Nullable String sn;
+
+    public QbusRolHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Main initialization
+     */
+    @Override
+    public void initialize() {
+        readConfig();
+
+        this.rolId = getId();
+
+        setSN();
+
+        scheduler.submit(() -> {
+            QbusCommunication controllerComm;
+
+            if (this.rolId != null) {
+                controllerComm = getCommunication("Screen/Store", this.rolId);
+            } else {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for Screen/Store no set! " + this.rolId);
+                return;
+            }
+
+            if (controllerComm == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "ID for Screen/Store not known in controller " + this.rolId);
+                return;
+            }
+
+            Map<Integer, QbusRol> rolCommLocal = controllerComm.getRol();
+
+            QbusRol outputLocal = rolCommLocal.get(this.rolId);
+
+            if (outputLocal == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "Bridge could not initialize Screen/Store ID " + this.rolId);
+                return;
+            }
+
+            outputLocal.setThingHandler(this);
+            handleStateUpdate(outputLocal);
+
+            QbusBridgeHandler qBridgeHandler = getBridgeHandler("Screen/Store", this.rolId);
+
+            if (qBridgeHandler != null) {
+                if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) {
+                    updateStatus(ThingStatus.ONLINE);
+                } else {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
+                            "Bridge offline for SCREEN/STORE ID " + this.rolId);
+                }
+            }
+        });
+    }
+
+    /**
+     * Handle the status update from the thing
+     */
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        QbusCommunication qComm = getCommunication("Screen/Store", this.rolId);
+
+        if (qComm == null) {
+            thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                    "ID for ROLLERSHUTTER/SCREEN not known in controller " + this.rolId);
+            return;
+        } else {
+            Map<Integer, QbusRol> rolComm = qComm.getRol();
+
+            QbusRol qRol = rolComm.get(this.rolId);
+
+            if (qRol == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "ID for ROLLERSHUTTER/SCREEN not known in controller " + this.rolId);
+                return;
+            } else {
+                scheduler.submit(() -> {
+                    if (!qComm.communicationActive()) {
+                        restartCommunication(qComm, "Screen/Store", this.rolId);
+                    }
+
+                    if (qComm.communicationActive()) {
+                        if (command == REFRESH) {
+                            handleStateUpdate(qRol);
+                            return;
+                        }
+
+                        switch (channelUID.getId()) {
+                            case CHANNEL_ROLLERSHUTTER:
+                                try {
+                                    handleScreenposCommand(qRol, command);
+                                } catch (IOException e) {
+                                    String message = e.getMessage();
+                                    logger.warn("Error on executing Rollershutter for screen ID {}. IOException: {}",
+                                            this.rolId, message);
+                                } catch (InterruptedException e) {
+                                    String message = e.toString();
+                                    logger.warn(
+                                            "Error on executing Rollershutter for screen ID {}. Interruptedexception {}",
+                                            this.rolId, message);
+                                }
+                                break;
+
+                            case CHANNEL_SLATS:
+                                try {
+                                    handleSlatsposCommand(qRol, command);
+                                } catch (IOException e) {
+                                    String message = e.getMessage();
+                                    logger.warn("Error on executing Slats for screen ID {}. IOException: {}",
+                                            this.rolId, message);
+                                } catch (InterruptedException e) {
+                                    String message = e.toString();
+                                    logger.warn("Error on executing Slats for screen ID {}. Interruptedexception {}",
+                                            this.rolId, message);
+                                }
+                                break;
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * Executes the command for screen up/down position
+     *
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    private void handleScreenposCommand(QbusRol qRol, Command command) throws InterruptedException, IOException {
+        String snr = getSN();
+        if (snr != null) {
+            if (command instanceof UpDownType) {
+                UpDownType upDown = (UpDownType) command;
+                if (upDown == DOWN) {
+                    qRol.execute(0, snr);
+                } else {
+                    qRol.execute(100, snr);
+                }
+            } else if (command instanceof IncreaseDecreaseType) {
+                IncreaseDecreaseType inc = (IncreaseDecreaseType) command;
+                int stepValue = ((Number) getConfig().get(CONFIG_STEP_VALUE)).intValue();
+                Integer currentValue = qRol.getState();
+                int newValue;
+                int sendValue;
+                if (currentValue != null) {
+                    if (inc == IncreaseDecreaseType.INCREASE) {
+                        newValue = currentValue + stepValue;
+                        // round down to step multiple
+                        newValue = newValue - newValue % stepValue;
+                        sendValue = newValue > 100 ? 100 : newValue;
+                        qRol.execute(sendValue, snr);
+                    } else {
+                        newValue = currentValue - stepValue;
+                        // round up to step multiple
+                        newValue = newValue + newValue % stepValue;
+                        sendValue = newValue > 100 ? 100 : newValue;
+                        qRol.execute(sendValue, snr);
+                    }
+                }
+            } else if (command instanceof PercentType) {
+                PercentType p = (PercentType) command;
+                int pp = p.intValue();
+                if (p == PercentType.ZERO) {
+                    qRol.execute(0, snr);
+                } else {
+                    qRol.execute(pp, snr);
+                }
+            }
+        }
+    }
+
+    /**
+     * Executes the command for screen slats position
+     *
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    private void handleSlatsposCommand(QbusRol qRol, Command command) throws InterruptedException, IOException {
+        String snr = getSN();
+        if (snr != null) {
+            if (command instanceof UpDownType) {
+                if (command == DOWN) {
+                    qRol.executeSlats(0, snr);
+                } else {
+                    qRol.executeSlats(100, snr);
+                }
+            } else if (command instanceof IncreaseDecreaseType) {
+                int stepValue = ((Number) getConfig().get(CONFIG_STEP_VALUE)).intValue();
+                Integer currentValue = qRol.getState();
+                int newValue;
+                int sendValue;
+                if (currentValue != null) {
+                    if (command == IncreaseDecreaseType.INCREASE) {
+                        newValue = currentValue + stepValue;
+                        // round down to step multiple
+                        newValue = newValue - newValue % stepValue;
+                        sendValue = newValue > 100 ? 100 : newValue;
+                        qRol.executeSlats(sendValue, snr);
+                    } else {
+                        newValue = currentValue - stepValue;
+                        // round up to step multiple
+                        newValue = newValue + newValue % stepValue;
+                        sendValue = newValue > 100 ? 100 : newValue;
+                        qRol.executeSlats(sendValue, snr);
+                    }
+                }
+            } else if (command instanceof PercentType) {
+                int percentToInt = ((PercentType) command).intValue();
+                if (command == PercentType.ZERO) {
+                    qRol.executeSlats(0, snr);
+                } else {
+                    qRol.executeSlats(percentToInt, snr);
+                }
+            }
+        }
+    }
+
+    /**
+     * Method to update state of channel, called from Qbus Screen/Store.
+     */
+    public void handleStateUpdate(QbusRol qRol) {
+        Integer rolState = qRol.getState();
+        Integer slatState = qRol.getStateSlats();
+
+        if (rolState != null) {
+            updateState(CHANNEL_ROLLERSHUTTER, new PercentType(rolState));
+        }
+        if (slatState != null) {
+            updateState(CHANNEL_SLATS, new PercentType(slatState));
+        }
+    }
+
+    /**
+     * Returns the serial number of the controller
+     *
+     * @return the serial nr
+     */
+    public @Nullable String getSN() {
+        return sn;
+    }
+
+    /**
+     * Sets the serial number of the controller
+     */
+    public void setSN() {
+        QbusBridgeHandler qBridgeHandler = getBridgeHandler("Screen/Store", this.rolId);
+        if (qBridgeHandler == null) {
+            thingOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                    "No communication with Qbus Bridge for ROLLERSHUTTER/SCREEN " + this.rolId);
+            return;
+        }
+        sn = qBridgeHandler.getSn();
+    }
+
+    /**
+     * Read the configuration
+     */
+    protected synchronized void readConfig() {
+        rolConfig = getConfig().as(QbusThingsConfig.class);
+    }
+
+    /**
+     * Returns the Id from the configuration
+     *
+     * @return outputId
+     */
+    public @Nullable Integer getId() {
+        QbusThingsConfig localConfig = rolConfig;
+        if (localConfig != null) {
+            return localConfig.rolId;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java
new file mode 100644 (file)
index 0000000..fc0381f
--- /dev/null
@@ -0,0 +1,217 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.handler;
+
+import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_SCENE;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.QbusBridgeHandler;
+import org.openhab.binding.qbus.internal.protocol.QbusCommunication;
+import org.openhab.binding.qbus.internal.protocol.QbusScene;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link QbusSceneHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public class QbusSceneHandler extends QbusGlobalHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(QbusSceneHandler.class);
+
+    protected @Nullable QbusThingsConfig sceneConfig = new QbusThingsConfig();
+
+    private @Nullable Integer sceneId;
+
+    private @Nullable String sn;
+
+    public QbusSceneHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Main initialization
+     */
+    @Override
+    public void initialize() {
+        readConfig();
+
+        this.sceneId = getId();
+
+        setSN();
+
+        scheduler.submit(() -> {
+            QbusCommunication controllerComm;
+
+            if (this.sceneId != null) {
+                controllerComm = getCommunication("Scene", this.sceneId);
+            } else {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for SCENE no set! " + this.sceneId);
+                return;
+            }
+
+            if (controllerComm == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "ID for SCENE not known in controller " + this.sceneId);
+                return;
+            }
+
+            Map<Integer, QbusScene> sceneCommLocal = controllerComm.getScene();
+
+            QbusScene outputLocal = sceneCommLocal.get(this.sceneId);
+
+            if (outputLocal == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "Bridge could not initialize SCENE ID " + this.sceneId);
+                return;
+            }
+
+            outputLocal.setThingHandler(this);
+
+            QbusBridgeHandler qBridgeHandler = getBridgeHandler("Scene", this.sceneId);
+
+            if ((qBridgeHandler != null) && (qBridgeHandler.getStatus() == ThingStatus.ONLINE)) {
+                updateStatus(ThingStatus.ONLINE);
+            } else {
+                thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Bridge offline for SCENE ID " + this.sceneId);
+            }
+        });
+    }
+
+    /**
+     * Handle the status update from the thing
+     */
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        QbusCommunication qComm = getCommunication("Scene", this.sceneId);
+
+        if (qComm == null) {
+            thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for SCENE not known in controller " + this.sceneId);
+            return;
+        } else {
+            Map<Integer, QbusScene> sceneComm = qComm.getScene();
+            QbusScene qScene = sceneComm.get(this.sceneId);
+
+            if (qScene == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "ID for SCENE not known in controller " + this.sceneId);
+                return;
+            } else {
+                scheduler.submit(() -> {
+                    if (!qComm.communicationActive()) {
+                        restartCommunication(qComm, "Scene", this.sceneId);
+                    }
+
+                    if (qComm.communicationActive()) {
+                        switch (channelUID.getId()) {
+                            case CHANNEL_SCENE:
+                                try {
+                                    handleSwitchCommand(qScene, channelUID, command);
+                                } catch (IOException e) {
+                                    String message = e.getMessage();
+                                    logger.warn("Error on executing Scene for scene ID {}. IOException: {}",
+                                            this.sceneId, message);
+                                } catch (InterruptedException e) {
+                                    String message = e.getMessage();
+                                    logger.warn("Error on executing Scene for scene ID {}. Interruptedexception {}",
+                                            this.sceneId, message);
+                                }
+                                break;
+
+                            default:
+                                thingOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                                        "Unknown Channel " + channelUID.getId());
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * Executes the scene command
+     *
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    void handleSwitchCommand(QbusScene qScene, ChannelUID channelUID, Command command)
+            throws InterruptedException, IOException {
+        String snr = getSN();
+        if (snr != null) {
+            if (command instanceof OnOffType) {
+                if (command == OnOffType.OFF) {
+                    qScene.execute(0, snr);
+                } else {
+                    qScene.execute(100, snr);
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the serial number of the controller
+     *
+     * @return the serial nr
+     */
+    public @Nullable String getSN() {
+        return sn;
+    }
+
+    /**
+     * Sets the serial number of the controller
+     */
+    public void setSN() {
+        QbusBridgeHandler qBridgeHandler = getBridgeHandler("Scene", this.sceneId);
+        if (qBridgeHandler == null) {
+            thingOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                    "No communication with Qbus Bridge for SCENE " + this.sceneId);
+            return;
+        }
+        sn = qBridgeHandler.getSn();
+    }
+
+    /**
+     * Read the configuration
+     */
+    protected synchronized void readConfig() {
+        sceneConfig = getConfig().as(QbusThingsConfig.class);
+    }
+
+    /**
+     * Returns the Id from the configuration
+     *
+     * @return outputId
+     */
+    public @Nullable Integer getId() {
+        QbusThingsConfig localConfig = sceneConfig;
+        if (localConfig != null) {
+            return localConfig.sceneId;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java
new file mode 100644 (file)
index 0000000..cbef42e
--- /dev/null
@@ -0,0 +1,295 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.handler;
+
+import static org.openhab.binding.qbus.internal.QbusBindingConstants.*;
+import static org.openhab.core.library.unit.SIUnits.CELSIUS;
+import static org.openhab.core.types.RefreshType.REFRESH;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.QbusBridgeHandler;
+import org.openhab.binding.qbus.internal.protocol.QbusCommunication;
+import org.openhab.binding.qbus.internal.protocol.QbusThermostat;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link QbusThermostatHandler} is responsible for handling the Thermostat outputs of Qbus
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public class QbusThermostatHandler extends QbusGlobalHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(QbusThermostatHandler.class);
+
+    protected @Nullable QbusThingsConfig thermostatConfig = new QbusThingsConfig();
+
+    private @Nullable Integer thermostatId;
+
+    private @Nullable String sn;
+
+    public QbusThermostatHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Main initialization
+     */
+    @Override
+    public void initialize() {
+        readConfig();
+
+        this.thermostatId = getId();
+
+        setSN();
+
+        scheduler.submit(() -> {
+            QbusCommunication controllerComm;
+
+            if (this.thermostatId != null) {
+                controllerComm = getCommunication("Thermostat", this.thermostatId);
+            } else {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for THERMOSTAT no set! " + this.thermostatId);
+                return;
+            }
+
+            if (controllerComm == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "ID for THERMOSTAT not known in controller " + this.thermostatId);
+                return;
+            }
+
+            Map<Integer, QbusThermostat> thermostatlCommLocal = controllerComm.getThermostat();
+
+            QbusThermostat outputLocal = thermostatlCommLocal.get(this.thermostatId);
+
+            if (outputLocal == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "Bridge could not initialize THERMOSTAT ID " + this.thermostatId);
+                return;
+            }
+
+            outputLocal.setThingHandler(this);
+            handleStateUpdate(outputLocal);
+
+            QbusBridgeHandler qBridgeHandler = getBridgeHandler("Thermostat", this.thermostatId);
+
+            if (qBridgeHandler != null) {
+                if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) {
+                    updateStatus(ThingStatus.ONLINE);
+                } else {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
+                            "Bridge offline for THERMOSTAT ID " + this.thermostatId);
+                }
+            }
+        });
+    }
+
+    /**
+     * Handle the status update from the thermostat
+     */
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        QbusCommunication qComm = getCommunication("Thermostat", this.thermostatId);
+
+        if (qComm == null) {
+            thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                    "ID for THERMOSTAT not known in controller " + this.thermostatId);
+            return;
+        } else {
+            Map<Integer, QbusThermostat> thermostatComm = qComm.getThermostat();
+
+            QbusThermostat qThermostat = thermostatComm.get(this.thermostatId);
+
+            if (qThermostat == null) {
+                thingOffline(ThingStatusDetail.CONFIGURATION_ERROR,
+                        "ID for THERMOSTAT not known in controller " + this.thermostatId);
+                return;
+            } else {
+                scheduler.submit(() -> {
+                    if (!qComm.communicationActive()) {
+                        restartCommunication(qComm, "Thermostat", this.thermostatId);
+                    }
+
+                    if (qComm.communicationActive()) {
+                        if (command == REFRESH) {
+                            handleStateUpdate(qThermostat);
+                            return;
+                        }
+
+                        switch (channelUID.getId()) {
+                            case CHANNEL_MODE:
+                                try {
+                                    handleModeCommand(qThermostat, command);
+                                } catch (IOException e) {
+                                    String message = e.getMessage();
+                                    logger.warn("Error on executing Mode for thermostat ID {}. IOException: {} ",
+                                            this.thermostatId, message);
+                                } catch (InterruptedException e) {
+                                    String message = e.getMessage();
+                                    logger.warn(
+                                            "Error on executing Mode for thermostat ID {}. Interruptedexception {} ",
+                                            this.thermostatId, message);
+                                }
+                                break;
+
+                            case CHANNEL_SETPOINT:
+                                try {
+                                    handleSetpointCommand(qThermostat, command);
+                                } catch (IOException e) {
+                                    String message = e.getMessage();
+                                    logger.warn("Error on executing Setpoint for thermostat ID {}. IOException: {} ",
+                                            this.thermostatId, message);
+                                } catch (InterruptedException e) {
+                                    String message = e.getMessage();
+                                    logger.warn(
+                                            "Error on executing Setpoint for thermostat ID {}. Interruptedexception {} ",
+                                            this.thermostatId, message);
+                                }
+                                break;
+
+                            default:
+                                thingOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                                        "Unknown Channel " + channelUID.getId());
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * Executes the Mode command
+     *
+     * @param qThermostat
+     * @param command
+     * @param snr
+     * @throws InterruptedException
+     * @throws IOException
+     */
+    private void handleModeCommand(QbusThermostat qThermostat, Command command)
+            throws InterruptedException, IOException {
+        String snr = getSN();
+        if (snr != null) {
+            if (command instanceof DecimalType) {
+                int mode = ((DecimalType) command).intValue();
+                qThermostat.executeMode(mode, snr);
+            }
+        }
+    }
+
+    /**
+     * Executes the Setpoint command
+     *
+     * @param qThermostat
+     * @param command
+     * @param snr
+     * @throws InterruptedException
+     * @throws IOException
+     */
+    private void handleSetpointCommand(QbusThermostat qThermostat, Command command)
+            throws InterruptedException, IOException {
+        String snr = getSN();
+        if (snr != null) {
+            if (command instanceof QuantityType<?>) {
+                QuantityType<?> s = (QuantityType<?>) command;
+                double sp = s.doubleValue();
+                QuantityType<?> spCelcius = s.toUnit(CELSIUS);
+
+                if (spCelcius != null) {
+                    qThermostat.executeSetpoint(sp, snr);
+                } else {
+                    logger.warn("Could not set setpoint for thermostat (conversion failed)  {}", this.thermostatId);
+                }
+            }
+        }
+    }
+
+    /**
+     * Method to update state of all channels, called from Qbus thermostat.
+     *
+     * @param qThermostat
+     */
+    public void handleStateUpdate(QbusThermostat qThermostat) {
+        Double measured = qThermostat.getMeasured();
+        if (measured != null) {
+            updateState(CHANNEL_MEASURED, new QuantityType<>(measured, CELSIUS));
+        }
+
+        Double setpoint = qThermostat.getSetpoint();
+        if (setpoint != null) {
+            updateState(CHANNEL_SETPOINT, new QuantityType<>(setpoint, CELSIUS));
+        }
+
+        Integer mode = qThermostat.getMode();
+        if (mode != null) {
+            updateState(CHANNEL_MODE, new DecimalType(mode));
+        }
+    }
+
+    /**
+     * Returns the serial number of the controller
+     *
+     * @return the serial nr
+     */
+    public @Nullable String getSN() {
+        return sn;
+    }
+
+    /**
+     * Sets the serial number of the controller
+     */
+    public void setSN() {
+        QbusBridgeHandler qBridgeHandler = getBridgeHandler("Thermostsat", this.thermostatId);
+        if (qBridgeHandler == null) {
+            thingOffline(ThingStatusDetail.COMMUNICATION_ERROR,
+                    "No communication with Qbus Bridge for THERMOSTAT " + this.thermostatId);
+            return;
+        }
+        sn = qBridgeHandler.getSn();
+    }
+
+    /**
+     * Read the configuration
+     */
+    protected synchronized void readConfig() {
+        thermostatConfig = getConfig().as(QbusThingsConfig.class);
+    }
+
+    /**
+     * Returns the Id from the configuration
+     *
+     * @return outputId
+     */
+    public @Nullable Integer getId() {
+        QbusThingsConfig localConfig = thermostatConfig;
+        if (localConfig != null) {
+            return localConfig.thermostatId;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java
new file mode 100644 (file)
index 0000000..56de0cb
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link QbusThingsConfig} is responible for handling configurations for all things
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public class QbusThingsConfig {
+    public @Nullable Integer bistabielId;
+    public @Nullable Integer dimmerId;
+    public @Nullable Integer co2Id;
+    public @Nullable Integer rolId;
+    public @Nullable Integer sceneId;
+    public @Nullable Integer thermostatId;
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java
new file mode 100644 (file)
index 0000000..a55ec87
--- /dev/null
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.protocol;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler;
+
+/**
+ * The {@link QbusBistabiel} class represents the Qbus BISTABIEL output.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public final class QbusBistabiel {
+
+    private @Nullable QbusCommunication qComm;
+
+    private Integer id;
+
+    private @Nullable Integer state;
+
+    private @Nullable QbusBistabielHandler thingHandler;
+
+    QbusBistabiel(Integer id) {
+        this.id = id;
+    }
+
+    /**
+     * This method should be called if the ThingHandler for the thing corresponding to this bistabiel is initialized.
+     * It keeps a record of the thing handler in this object so the thing can be updated when
+     * the bistable output receives an update from the Qbus client.
+     *
+     * @param handler
+     */
+    public void setThingHandler(QbusBistabielHandler handler) {
+        this.thingHandler = handler;
+    }
+
+    /**
+     * This method sets a pointer to the qComm BISTABIEL of class {@link QbusCommuncation}.
+     * This is then used to be able to call back the sendCommand method in this class to send a command to the
+     * Qbus client.
+     *
+     * @param qComm
+     */
+    public void setQComm(QbusCommunication qComm) {
+        this.qComm = qComm;
+    }
+
+    /**
+     * Update the value of the Bistabiel.
+     *
+     * @param state
+     */
+    void updateState(@Nullable Integer state) {
+        this.state = state;
+        QbusBistabielHandler handler = this.thingHandler;
+        if (handler != null) {
+            handler.handleStateUpdate(this);
+        }
+    }
+
+    /**
+     * Get the value of the Bistabiel.
+     *
+     * @return
+     */
+    public @Nullable Integer getState() {
+        return this.state;
+    }
+
+    /**
+     * Sends Bistabiel state to Qbus.
+     *
+     * @param value
+     * @param sn
+     * @throws InterruptedException
+     * @throws IOException
+     */
+    public void execute(int value, String sn) throws InterruptedException, IOException {
+        QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeBistabiel").withId(this.id).withState(value);
+        QbusCommunication comm = this.qComm;
+        if (comm != null) {
+            comm.sendMessage(qCmd);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java
new file mode 100644 (file)
index 0000000..88d0f40
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.qbus.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.handler.QbusCO2Handler;
+
+/**
+ * The {@link QbusCO2} class represents the action Qbus CO2 output.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public final class QbusCO2 {
+
+    private @Nullable Integer state;
+
+    private @Nullable QbusCO2Handler thingHandler;
+
+    /**
+     * This method should be called if the ThingHandler for the thing corresponding to this CO2 is initialized.
+     * It keeps a record of the thing handler in this object so the thing can be updated when
+     * the CO2 output receives an update from the Qbus IP-interface.
+     *
+     * @param handler
+     */
+    public void setThingHandler(QbusCO2Handler handler) {
+        this.thingHandler = handler;
+    }
+
+    /**
+     * Get state of CO2.
+     *
+     * @return CO2 state
+     */
+    public @Nullable Integer getState() {
+        return this.state;
+    }
+
+    /**
+     * Update the value of the CO2.
+     *
+     * @param CO2 value
+     */
+    void updateState(@Nullable Integer state) {
+        this.state = state;
+        QbusCO2Handler handler = this.thingHandler;
+        if (handler != null) {
+            handler.handleStateUpdate(this);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java
new file mode 100644 (file)
index 0000000..e07032d
--- /dev/null
@@ -0,0 +1,796 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.protocol;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.QbusBridgeHandler;
+import org.openhab.core.common.NamedThreadFactory;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+
+/**
+ * The {@link QbusCommunication} class is able to do the following tasks with Qbus
+ * CTD controllers:
+ * <ul>
+ * <li>Start and stop TCP socket connection with Qbus Server.
+ * <li>Read all the outputs and their status from the Qbus Controller.
+ * <li>Execute Qbus commands.
+ * <li>Listen to events from Qbus.
+ * </ul>
+ *
+ * A class instance is instantiated from the {@link QbusBridgeHandler} class initialization.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public final class QbusCommunication extends BaseThingHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(QbusCommunication.class);
+
+    private @Nullable Socket qSocket;
+    private @Nullable PrintWriter qOut;
+    private @Nullable BufferedReader qIn;
+
+    private boolean listenerStopped;
+    private boolean qbusListenerRunning;
+
+    private Gson gsonOut = new Gson();
+    private Gson gsonIn;
+
+    private @Nullable String ctd;
+    private boolean ctdConnected;
+
+    private List<Map<String, String>> outputs = new ArrayList<>();
+    private final Map<Integer, QbusBistabiel> bistabiel = new HashMap<>();
+    private final Map<Integer, QbusScene> scene = new HashMap<>();
+    private final Map<Integer, QbusDimmer> dimmer = new HashMap<>();
+    private final Map<Integer, QbusRol> rol = new HashMap<>();
+    private final Map<Integer, QbusThermostat> thermostat = new HashMap<>();
+    private final Map<Integer, QbusCO2> co2 = new HashMap<>();
+
+    private final ExecutorService threadExecutor = Executors
+            .newSingleThreadExecutor(new NamedThreadFactory(getThing().getUID().getAsString(), true));
+
+    private @Nullable QbusBridgeHandler bridgeCallBack;
+
+    public QbusCommunication(Thing thing) {
+        super(thing);
+        GsonBuilder gsonBuilder = new GsonBuilder();
+        gsonBuilder.registerTypeAdapter(QbusMessageBase.class, new QbusMessageDeserializer());
+        gsonIn = gsonBuilder.create();
+    }
+
+    /**
+     * Starts main communication thread.
+     * <ul>
+     * <li>Connect to Qbus server
+     * <li>Requests outputs
+     * <li>Start listener
+     * </ul>
+     *
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    public synchronized void startCommunication() throws IOException, InterruptedException {
+        QbusBridgeHandler handler = bridgeCallBack;
+        ctdConnected = false;
+
+        if (qbusListenerRunning) {
+            throw new IOException("Previous listening thread is still active.");
+        }
+
+        if (handler == null) {
+            throw new IOException("No Bridge handler initialised.");
+        }
+
+        InetAddress addr = InetAddress.getByName(handler.getAddress());
+        Integer port = handler.getPort();
+
+        if (port != null) {
+            Socket socket = new Socket(addr, port);
+            qSocket = socket;
+            qOut = new PrintWriter(socket.getOutputStream(), true);
+            qIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+        } else {
+            return;
+        }
+
+        setSN();
+        getSN();
+
+        // Connect to Qbus server
+        connect();
+
+        // Then start thread to listen to incoming updates from Qbus.
+        threadExecutor.execute(() -> {
+            try {
+                qbusListener();
+            } catch (IOException e) {
+                String msg = e.getMessage();
+                logger.warn("Could not start listening thread, IOException: {}", msg);
+            } catch (InterruptedException e) {
+                String msg = e.getMessage();
+                logger.warn("Could not start listening thread, InterruptedException: {}", msg);
+            }
+        });
+
+        if (!ctdConnected) {
+            handler.bridgePending("Waiting for CTD to come online...");
+        }
+    }
+
+    /**
+     * Cleanup socket when the communication with Qbus Server is closed.
+     *
+     * @throws IOException
+     *
+     */
+    public synchronized void stopCommunication() throws IOException {
+        listenerStopped = true;
+
+        Socket socket = qSocket;
+
+        if (socket != null) {
+            try {
+                socket.close();
+            } catch (IOException ignore) {
+                // ignore IO Error when trying to close the socket if the intention is to close it anyway
+            }
+        }
+
+        BufferedReader reader = this.qIn;
+        if (reader != null) {
+            reader.close();
+        }
+
+        PrintWriter writer = this.qOut;
+        if (writer != null) {
+            writer.close();
+        }
+
+        qSocket = null;
+        qbusListenerRunning = false;
+        ctdConnected = false;
+
+        logger.trace("Communication stopped from thread {}", Thread.currentThread().getId());
+    }
+
+    /**
+     * Close and restart communication with Qbus Server.
+     *
+     * @throws InterruptedException
+     * @throws IOException
+     */
+    public synchronized void restartCommunication() throws InterruptedException, IOException {
+        stopCommunication();
+
+        startCommunication();
+    }
+
+    /**
+     * Thread that handles incoming messages from Qbus client.
+     * <p>
+     * The thread listens to the TCP socket opened at instantiation of the {@link QbusCommunication} class
+     * and interprets all incomming json messages. It triggers state updates for active channels linked to the
+     * Qbus outputs. It is started after initialization of the communication.
+     *
+     * @return
+     * @throws IOException
+     * @throws InterruptedException
+     *
+     *
+     */
+    private void qbusListener() throws IOException, InterruptedException {
+        String qMessage;
+
+        listenerStopped = false;
+        qbusListenerRunning = true;
+
+        BufferedReader reader = this.qIn;
+
+        if (reader == null) {
+            throw new IOException("Bufferreader for incoming messages not initialized.");
+        }
+
+        try {
+            while (!Thread.currentThread().isInterrupted() && ((qMessage = reader.readLine()) != null)) {
+                readMessage(qMessage);
+
+            }
+        } catch (IOException e) {
+            if (!listenerStopped) {
+                qbusListenerRunning = false;
+                // the IO has stopped working, so we need to close cleanly and try to restart
+                restartCommunication();
+                return;
+            }
+        } finally {
+            qbusListenerRunning = false;
+        }
+
+        if (!listenerStopped) {
+            qbusListenerRunning = false;
+
+            QbusBridgeHandler handler = bridgeCallBack;
+
+            if (handler != null) {
+                ctdConnected = false;
+                handler.bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, "No communication with Qbus server");
+            }
+        }
+
+        qbusListenerRunning = false;
+        logger.trace("Event listener thread stopped on thread {}", Thread.currentThread().getId());
+    };
+
+    /**
+     * Called by other methods to send json data to Qbus.
+     *
+     * @param qMessage
+     * @throws InterruptedException
+     * @throws IOException
+     */
+    synchronized void sendMessage(Object qMessage) throws InterruptedException, IOException {
+        PrintWriter writer = qOut;
+        String json = gsonOut.toJson(qMessage);
+
+        if (writer != null) {
+            writer.println(json);
+            // Delay after sending data to improve scene execution
+            TimeUnit.MILLISECONDS.sleep(250);
+        }
+
+        if ((writer == null) || (writer.checkError())) {
+            logger.warn("Error sending message, trying to restart communication");
+
+            restartCommunication();
+
+            // retry sending after restart
+            writer = qOut;
+            if (writer != null) {
+                writer.println(json);
+            }
+            if ((writer == null) || (writer.checkError())) {
+                logger.warn("Error resending message");
+
+            }
+        }
+    }
+
+    /**
+     * Method that interprets all feedback from Qbus Server application and calls appropriate handling methods.
+     * <ul>
+     * <li>Get request & update states for Bistabiel/Timers/Intervals/Mono outputs
+     * <li>Get request & update states for the Scenes
+     * <li>Get request & update states for Dimmers 1T and 2T
+     * <li>Get request & update states for Shutters
+     * <li>Get request & update states for Thermostats
+     * <li>Get request & update states for CO2
+     * </ul>
+     *
+     * @param qMessage message read from Qbus.
+     * @throws InterruptedException
+     * @throws IOException
+     *
+     */
+    private void readMessage(String qMessage) {
+        String sn = null;
+        String cmd = "";
+        String ctd = null;
+        Integer id = null;
+        Integer state = null;
+        Integer mode = null;
+        Double setpoint = null;
+        Double measured = null;
+        Integer slats = null;
+
+        QbusMessageBase qMessageGson;
+        try {
+            qMessageGson = gsonIn.fromJson(qMessage, QbusMessageBase.class);
+
+            if (qMessageGson != null) {
+                ctd = qMessageGson.getSn();
+                cmd = qMessageGson.getCmd();
+                id = qMessageGson.getId();
+                state = qMessageGson.getState();
+                mode = qMessageGson.getMode();
+                setpoint = qMessageGson.getSetPoint();
+                measured = qMessageGson.getMeasured();
+                slats = qMessageGson.getSlatState();
+            }
+        } catch (JsonParseException e) {
+            String msg = e.getMessage();
+            logger.trace("Not acted on unsupported json {} : {}", qMessage, msg);
+            return;
+        }
+
+        QbusBridgeHandler handler = bridgeCallBack;
+
+        if (handler != null) {
+            sn = handler.getSn();
+        }
+
+        if (sn != null && ctd != null) {
+            try {
+                if (sn.equals(ctd) && qMessageGson != null) { // Check if commands are for this Bridge
+                    // Handle all outputs from Qbus
+                    if ("returnOutputs".equals(cmd)) {
+                        outputs = ((QbusMessageListMap) qMessageGson).getOutputs();
+
+                        for (Map<String, String> ctdOutputs : outputs) {
+
+                            String ctdType = ctdOutputs.get("type");
+                            String ctdIdStr = ctdOutputs.get("id");
+                            Integer ctdId = null;
+
+                            if (ctdIdStr != null) {
+                                ctdId = Integer.parseInt(ctdIdStr);
+                            } else {
+                                return;
+                            }
+
+                            if (ctdType != null) {
+                                String ctdState = ctdOutputs.get("state");
+                                String ctdMmode = ctdOutputs.get("regime");
+                                String ctdSetpoint = ctdOutputs.get("setpoint");
+                                String ctdMeasured = ctdOutputs.get("measured");
+                                String ctdSlats = ctdOutputs.get("slats");
+
+                                Integer ctdStateI = null;
+                                if (ctdState != null) {
+                                    ctdStateI = Integer.parseInt(ctdState);
+                                }
+
+                                Integer ctdSlatsI = null;
+                                if (ctdSlats != null) {
+                                    ctdSlatsI = Integer.parseInt(ctdSlats);
+                                }
+
+                                Integer ctdMmodeI = null;
+                                if (ctdMmode != null) {
+                                    ctdMmodeI = Integer.parseInt(ctdMmode);
+                                }
+
+                                Double ctdSetpointD = null;
+                                if (ctdSetpoint != null) {
+                                    ctdSetpointD = Double.parseDouble(ctdSetpoint);
+                                }
+
+                                Double ctdMeasuredD = null;
+                                if (ctdMeasured != null) {
+                                    ctdMeasuredD = Double.parseDouble(ctdMeasured);
+                                }
+
+                                if (ctdState != null) {
+                                    if (ctdType.equals("bistabiel")) {
+                                        QbusBistabiel output = new QbusBistabiel(ctdId);
+                                        if (!bistabiel.containsKey(ctdId)) {
+                                            output.setQComm(this);
+                                            output.updateState(ctdStateI);
+                                            bistabiel.put(ctdId, output);
+                                        } else {
+                                            output.updateState(ctdStateI);
+                                        }
+                                    } else if (ctdType.equals("dimmer")) {
+                                        QbusDimmer output = new QbusDimmer(ctdId);
+                                        if (!dimmer.containsKey(ctdId)) {
+                                            output.setQComm(this);
+                                            output.updateState(ctdStateI);
+                                            dimmer.put(ctdId, output);
+                                        } else {
+                                            output.updateState(ctdStateI);
+                                        }
+                                    } else if (ctdType.equals("CO2")) {
+                                        QbusCO2 output = new QbusCO2();
+                                        if (!co2.containsKey(ctdId)) {
+                                            output.updateState(ctdStateI);
+                                            co2.put(ctdId, output);
+                                        } else {
+                                            output.updateState(ctdStateI);
+                                        }
+                                    } else if (ctdType.equals("scene")) {
+                                        QbusScene output = new QbusScene(ctdId);
+                                        if (!scene.containsKey(ctdId)) {
+                                            output.setQComm(this);
+                                            scene.put(ctdId, output);
+                                        }
+                                    } else if (ctdType.equals("rol")) {
+                                        QbusRol output = new QbusRol(ctdId);
+                                        if (!rol.containsKey(ctdId)) {
+                                            output.setQComm(this);
+                                            output.updateState(ctdStateI);
+                                            if (ctdSlats != null) {
+                                                output.updateSlats(ctdSlatsI);
+                                            }
+                                            rol.put(ctdId, output);
+                                        } else {
+                                            output.updateState(ctdStateI);
+                                            if (ctdSlats != null) {
+                                                output.updateSlats(ctdSlatsI);
+                                            }
+                                        }
+                                    }
+                                } else if (ctdMeasuredD != null && ctdSetpointD != null && ctdMmodeI != null) {
+                                    if (ctdType.equals("thermostat")) {
+                                        QbusThermostat output = new QbusThermostat(ctdId);
+                                        if (!thermostat.containsKey(ctdId)) {
+                                            output.setQComm(this);
+                                            output.updateState(ctdMeasuredD, ctdSetpointD, ctdMmodeI);
+                                            thermostat.put(ctdId, output);
+                                        } else {
+                                            output.updateState(ctdMeasuredD, ctdSetpointD, ctdMmodeI);
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                        // Handle update commands from Qbus
+                    } else if ("updateBistabiel".equals(cmd)) {
+                        if (id != null && state != null) {
+                            updateBistabiel(id, state);
+                        }
+                    } else if ("updateDimmer".equals(cmd)) {
+                        if (id != null && state != null) {
+                            updateDimmer(id, state);
+                        }
+                    } else if ("updateDimmer".equals(cmd)) {
+                        if (id != null && state != null) {
+                            updateDimmer(id, state);
+                        }
+                    } else if ("updateCo2".equals(cmd)) {
+                        if (id != null && state != null) {
+                            updateCO2(id, state);
+                        }
+                    } else if ("updateThermostat".equals(cmd)) {
+                        if (id != null && measured != null && setpoint != null && mode != null) {
+                            updateThermostat(id, mode, setpoint, measured);
+                        }
+                    } else if ("updateRol02p".equals(cmd)) {
+                        if (id != null && state != null) {
+                            updateRol(id, state);
+                        }
+                    } else if ("updateRol02pSlat".equals(cmd)) {
+                        if (id != null && state != null && slats != null) {
+                            updateRolSlats(id, state, slats);
+                        }
+                        // Incomming commands from Qbus server to verify the client connection
+                    } else if ("noconnection".equals(cmd)) {
+                        eventDisconnect();
+                    } else if ("connected".equals(cmd)) {
+                        // threadExecutor.execute(() -> {
+                        try {
+                            requestOutputs();
+                        } catch (InterruptedException e) {
+                            String msg = e.getMessage();
+                            logger.warn("Could not request outputs. InterruptedException: {}", msg);
+                        } catch (IOException e) {
+                            String msg = e.getMessage();
+                            logger.warn("Could not request outputs. IOException: {}", msg);
+                        }
+                    }
+                }
+            } catch (JsonParseException e) {
+                String msg = e.getMessage();
+                logger.warn("Not acted on unsupported json {}, {}", qMessage, msg);
+            }
+        }
+    }
+
+    /**
+     * Initialize the communication object
+     */
+    @Override
+    public void initialize() {
+    }
+
+    /**
+     * Initial connection to Qbus Server to open a communication channel
+     *
+     * @throws InterruptedException
+     * @throws IOException
+     */
+    private void connect() throws InterruptedException, IOException {
+        String snr = getSN();
+
+        if (snr != null) {
+            QbusMessageCmd qCmd = new QbusMessageCmd(snr, "openHAB");
+
+            sendMessage(qCmd);
+
+            BufferedReader reader = qIn;
+
+            if (reader == null) {
+                throw new IOException("Cannot read from socket, reader not connected.");
+            }
+            readMessage(reader.readLine());
+
+        } else {
+            QbusBridgeHandler handler = bridgeCallBack;
+            if (handler != null) {
+                handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined");
+            }
+        }
+    }
+
+    /**
+     * Send a request for all available outputs and initializes them via readMessage
+     *
+     * @throws InterruptedException
+     * @throws IOException
+     */
+    private void requestOutputs() throws InterruptedException, IOException {
+        String snr = getSN();
+        QbusBridgeHandler handler = bridgeCallBack;
+
+        if (snr != null) {
+            QbusMessageCmd qCmd = new QbusMessageCmd(snr, "all");
+            sendMessage(qCmd);
+
+            BufferedReader reader = qIn;
+            if (reader == null) {
+                throw new IOException("Cannot read from socket, reader not connected.");
+            }
+            readMessage(reader.readLine());
+            ctdConnected = true;
+
+            if (handler != null) {
+                handler.bridgeOnline();
+            }
+
+        } else {
+            if (handler != null) {
+                handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined");
+            }
+        }
+    }
+
+    /**
+     * Event on incoming Bistabiel/Timer/Mono/Interval updates
+     *
+     * @param id
+     * @param state
+     */
+    private void updateBistabiel(Integer id, Integer state) {
+        QbusBistabiel qBistabiel = this.bistabiel.get(id);
+
+        if (qBistabiel != null) {
+            qBistabiel.updateState(state);
+        } else {
+            logger.trace("Bistabiel in controller not known {}", id);
+        }
+    }
+
+    /**
+     * Event on incoming Dimmer updates
+     *
+     * @param id
+     * @param state
+     */
+    private void updateDimmer(Integer id, Integer state) {
+        QbusDimmer qDimmer = this.dimmer.get(id);
+
+        if (qDimmer != null) {
+            qDimmer.updateState(state);
+        } else {
+            logger.trace("Dimmer in controller not known {}", id);
+        }
+    }
+
+    /**
+     * Event on incoming thermostat updates
+     *
+     * @param id
+     * @param mode
+     * @param sp
+     * @param ct
+     */
+    private void updateThermostat(Integer id, int mode, double sp, double ct) {
+        QbusThermostat qThermostat = this.thermostat.get(id);
+
+        if (qThermostat != null) {
+            qThermostat.updateState(ct, sp, mode);
+        } else {
+            logger.trace("Thermostat in controller not known {}", id);
+        }
+    }
+
+    /**
+     * Event on incoming CO2 updates
+     *
+     * @param id
+     * @param state
+     */
+    private void updateCO2(Integer id, Integer state) {
+        QbusCO2 qCO2 = this.co2.get(id);
+
+        if (qCO2 != null) {
+            qCO2.updateState(state);
+        } else {
+            logger.trace("CO2 in controller not known {}", id);
+        }
+    }
+
+    /**
+     * Event on incoming screen updates
+     *
+     * @param id
+     * @param state
+     */
+    private void updateRol(Integer id, Integer state) {
+        QbusRol qRol = this.rol.get(id);
+
+        if (qRol != null) {
+            qRol.updateState(state);
+        } else {
+            logger.trace("ROL02P in controller not known {}", id);
+        }
+    }
+
+    /**
+     * Event on incoming screen with slats updates
+     *
+     * @param id
+     * @param state
+     * @param slats
+     */
+    private void updateRolSlats(Integer id, Integer state, Integer slats) {
+        QbusRol qRol = this.rol.get(id);
+
+        if (qRol != null) {
+            qRol.updateState(state);
+            qRol.updateSlats(slats);
+        } else {
+            logger.trace("ROL02P with slats in controller not known {}", id);
+        }
+    }
+
+    /**
+     * Put Bridge offline when there is no connection from the QbusClient
+     *
+     */
+    private void eventDisconnect() {
+        QbusBridgeHandler handler = bridgeCallBack;
+
+        if (handler != null) {
+            handler.bridgePending("Waiting for CTD connection");
+        }
+    }
+
+    /**
+     * Return all Bistabiel/Timers/Mono/Intervals in the Qbus Controller.
+     *
+     * @return
+     */
+    public Map<Integer, QbusBistabiel> getBistabiel() {
+        return this.bistabiel;
+    }
+
+    /**
+     * Return all Scenes in the Qbus Controller
+     *
+     * @return
+     */
+    public Map<Integer, QbusScene> getScene() {
+        return this.scene;
+    }
+
+    /**
+     * Return all Dimmers outputs in the Qbus Controller.
+     *
+     * @return
+     */
+    public Map<Integer, QbusDimmer> getDimmer() {
+        return this.dimmer;
+    }
+
+    /**
+     * Return all rollershutter/screen outputs in the Qbus Controller.
+     *
+     * @return
+     */
+    public Map<Integer, QbusRol> getRol() {
+        return this.rol;
+    }
+
+    /**
+     * Return all Thermostats outputs in the Qbus Controller.
+     *
+     * @return
+     */
+    public Map<Integer, QbusThermostat> getThermostat() {
+        return this.thermostat;
+    }
+
+    /**
+     * Return all CO2 outputs in the Qbus Controller.
+     *
+     * @return
+     */
+    public Map<Integer, QbusCO2> getCo2() {
+        return this.co2;
+    }
+
+    /**
+     * Method to check if communication with Qbus Server is active
+     *
+     * @return True if active
+     */
+    public boolean communicationActive() {
+        return qSocket != null;
+    }
+
+    /**
+     * Method to check if communication with Qbus Client is active
+     *
+     * @return True if active
+     */
+    public boolean clientConnected() {
+        return ctdConnected;
+    }
+
+    /**
+     * @param bridgeCallBack the bridgeCallBack to set
+     */
+    public void setBridgeCallBack(QbusBridgeHandler bridgeCallBack) {
+        this.bridgeCallBack = bridgeCallBack;
+    }
+
+    /**
+     * Get the serial number of the CTD as configured in the Bridge.
+     *
+     * @return serial number of controller
+     */
+    public @Nullable String getSN() {
+        return this.ctd;
+    }
+
+    /**
+     * Sets the serial number of the CTD, as configured in the Bridge.
+     */
+    public void setSN() {
+        QbusBridgeHandler qBridgeHandler = bridgeCallBack;
+
+        if (qBridgeHandler != null) {
+            this.ctd = qBridgeHandler.getSn();
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java
new file mode 100644 (file)
index 0000000..a7d6197
--- /dev/null
@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.protocol;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler;
+
+/**
+ * The {@link QbusDimmer} class represents the action Qbus Dimmer output.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public final class QbusDimmer {
+
+    private @Nullable QbusCommunication qComm;
+
+    private Integer id;
+
+    private @Nullable Integer state;
+
+    private @Nullable QbusDimmerHandler thingHandler;
+
+    QbusDimmer(Integer id) {
+        this.id = id;
+    }
+
+    /**
+     * This method should be called if the ThingHandler for the thing corresponding to this dimmer is initialized.
+     * It keeps a record of the thing handler in this object so the thing can be updated when
+     * the dimmer receives an update from the Qbus client.
+     *
+     * @param handler
+     */
+    public void setThingHandler(QbusDimmerHandler handler) {
+        this.thingHandler = handler;
+    }
+
+    /**
+     * This method sets a pointer to the qComm Dimmer of class {@link QbusCommuncation}.
+     * This is then used to be able to call back the sendCommand method in this class to send a command to the
+     * Qbus client.
+     *
+     * @param qComm
+     */
+    public void setQComm(QbusCommunication qComm) {
+        this.qComm = qComm;
+    }
+
+    /**
+     * Update the value of the dimmer
+     *
+     * @param state
+     */
+    public void updateState(@Nullable Integer state) {
+        this.state = state;
+        QbusDimmerHandler handler = this.thingHandler;
+        if (handler != null) {
+            handler.handleStateUpdate(this);
+        }
+    }
+
+    /**
+     * Get the state of dimmer.
+     *
+     * @return dimmer state
+     */
+    public @Nullable Integer getState() {
+        return this.state;
+    }
+
+    /**
+     * Sets the state of Dimmer.
+     *
+     * @param dimmer state
+     */
+    void setState(int state) {
+        this.state = state;
+        QbusDimmerHandler handler = thingHandler;
+        if (handler != null) {
+            handler.handleStateUpdate(this);
+        }
+    }
+
+    /**
+     * Sends the dimmer state to Qbus.
+     *
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    public void execute(int percent, String sn) throws InterruptedException, IOException {
+        QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeDimmer").withId(this.id).withState(percent);
+        QbusCommunication comm = this.qComm;
+        if (comm != null) {
+            comm.sendMessage(qCmd);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java
new file mode 100644 (file)
index 0000000..5c75d1c
--- /dev/null
@@ -0,0 +1,121 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Class {@link QbusMessageBase} used as base class for output from gson for cmd or event feedback from the Qbus server.
+ * This class only contains the common base fields required for the deserializer
+ * {@link QbusMessageDeserializer} to select the specific formats implemented in {@link QbusMessageMap},
+ * {@link QbusMessageListMap}, {@link QbusMessageCmd}.
+ * <p>
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+abstract class QbusMessageBase {
+
+    private @Nullable String ctd;
+    protected @Nullable String cmd;
+    protected @Nullable String type;
+    protected @Nullable Integer id;
+    protected @Nullable Integer state;
+    protected @Nullable Integer mode;
+    protected @Nullable Double setpoint;
+    protected @Nullable Double measured;
+    protected @Nullable Integer slatState;
+
+    @Nullable
+    String getSn() {
+        return this.ctd;
+    }
+
+    void setSn(String ctd) {
+        this.ctd = ctd;
+    }
+
+    @Nullable
+    String getCmd() {
+        return this.cmd;
+    }
+
+    void setCmd(String cmd) {
+        this.cmd = cmd;
+    }
+
+    @Nullable
+    public Integer getId() {
+        return id;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    @Nullable
+    public String getType() {
+        return type;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    @Nullable
+    public Integer getState() {
+        return state;
+    }
+
+    public void setState(int state) {
+        this.state = state;
+    }
+
+    @Nullable
+    public Integer getMode() {
+        return mode;
+    }
+
+    public void setMode(int mode) {
+        this.mode = mode;
+    }
+
+    @Nullable
+    public Double getSetPoint() {
+        return setpoint;
+    }
+
+    public void setSetPoint(Double setpoint) {
+        this.setpoint = setpoint;
+    }
+
+    @Nullable
+    public Double getMeasured() {
+        return measured;
+    }
+
+    public void setMeasured(Double measured) {
+        this.measured = measured;
+    }
+
+    @Nullable
+    public Integer getSlatState() {
+        return slatState;
+    }
+
+    public void setSlatState(int slatState) {
+        this.slatState = slatState;
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java
new file mode 100644 (file)
index 0000000..f7e7d03
--- /dev/null
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Class {@link QbusMessageCmd} used as input to gson to send commands to Qbus. Extends
+ * {@link QbusMessageBase}.
+ * <p>
+ * Example: <code>{"cmd":"executebistabiel","id":1,"value1":0}</code>
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+class QbusMessageCmd extends QbusMessageBase {
+
+    QbusMessageCmd(String CTD) {
+        super.setSn(CTD);
+    }
+
+    QbusMessageCmd(String CTD, String cmd) {
+        this(CTD);
+        this.cmd = cmd;
+    }
+
+    QbusMessageCmd withId(Integer id) {
+        this.setId(id);
+        return this;
+    }
+
+    QbusMessageCmd withState(int state) {
+        this.setState(state);
+        return this;
+    }
+
+    QbusMessageCmd withMode(int mode) {
+        this.setMode(mode);
+        return this;
+    }
+
+    QbusMessageCmd withSetPoint(Double setpoint) {
+        this.setSetPoint(setpoint);
+        return this;
+    }
+
+    QbusMessageCmd withSlatState(int slatState) {
+        this.setSlatState(slatState);
+        return this;
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java
new file mode 100644 (file)
index 0000000..2d06f72
--- /dev/null
@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.protocol;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+
+/**
+ * Class {@link QbusMessageDeserializer} deserializes all json messages from Qbus. Various json
+ * message formats are supported. The format is selected based on the content of the cmd and event json objects.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ *
+ */
+
+@NonNullByDefault
+class QbusMessageDeserializer implements JsonDeserializer<QbusMessageBase> {
+
+    @Override
+    public @Nullable QbusMessageBase deserialize(final JsonElement json, final Type typeOfT,
+            final JsonDeserializationContext context) throws JsonParseException {
+        final JsonObject jsonObject = json.getAsJsonObject();
+
+        String ctd = null;
+        String cmd = null;
+        Integer id = null;
+        Integer state = null;
+        Integer mode = null;
+        Double measured = null;
+        Double setpoint = null;
+        Integer slats = null;
+
+        QbusMessageBase message = null;
+
+        JsonElement jsonOutputs = null;
+        try {
+            if (jsonObject.has("CTD")) {
+                ctd = jsonObject.get("CTD").getAsString();
+            }
+
+            if (jsonObject.has("cmd")) {
+                cmd = jsonObject.get("cmd").getAsString();
+            }
+
+            if (jsonObject.has("id")) {
+                id = jsonObject.get("id").getAsInt();
+            }
+
+            if (jsonObject.has("state")) {
+                state = jsonObject.get("state").getAsInt();
+            }
+
+            if (jsonObject.has("mode")) {
+                mode = jsonObject.get("mode").getAsInt();
+            }
+
+            if (jsonObject.has("measured")) {
+                measured = jsonObject.get("measured").getAsDouble();
+            }
+
+            if (jsonObject.has("setpoint")) {
+                setpoint = jsonObject.get("setpoint").getAsDouble();
+            }
+
+            if (jsonObject.has("slats")) {
+                slats = jsonObject.get("slats").getAsInt();
+            }
+
+            if (jsonObject.has("outputs")) {
+                jsonOutputs = jsonObject.get("outputs");
+
+            }
+
+            if (ctd != null && cmd != null) {
+                if (jsonOutputs != null) {
+                    if (jsonOutputs.isJsonArray()) {
+                        JsonArray jsonOutputsArray = jsonOutputs.getAsJsonArray();
+                        message = new QbusMessageListMap();
+                        message.setCmd(cmd);
+                        message.setSn(ctd);
+
+                        List<Map<String, String>> outputsList = new ArrayList<>();
+                        for (int i = 0; i < jsonOutputsArray.size(); i++) {
+                            JsonObject jsonOutputsObject = jsonOutputsArray.get(i).getAsJsonObject();
+
+                            Map<String, String> outputs = new HashMap<>();
+                            for (Entry<String, JsonElement> entry : jsonOutputsObject.entrySet()) {
+                                outputs.put(entry.getKey(), entry.getValue().getAsString());
+                            }
+                            outputsList.add(outputs);
+                        }
+                        ((QbusMessageListMap) message).setOutputs(outputsList);
+                    }
+
+                } else {
+                    message = new QbusMessageMap();
+
+                    message.setCmd(cmd);
+                    message.setSn(ctd);
+
+                    if (id != null) {
+                        message.setId(id);
+                    }
+
+                    if (state != null) {
+                        message.setState(state);
+                    }
+
+                    if (slats != null) {
+                        message.setSlatState(slats);
+                    }
+
+                    if (mode != null) {
+                        message.setMode(mode);
+                    }
+
+                    if (measured != null) {
+                        message.setMeasured(measured);
+                    }
+
+                    if (setpoint != null) {
+                        message.setSetPoint(setpoint);
+                    }
+
+                }
+            }
+            return message;
+        } catch (IllegalStateException e) {
+            String mess = e.getMessage();
+            throw new JsonParseException("Unexpected Json format  " + mess + " for " + jsonObject.toString());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java
new file mode 100644 (file)
index 0000000..01fefbd
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.protocol;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Class {@link QbusMessageListMap} used as output from gson for cmd or event feedback from Qbus where the
+ * data part is enclosed by [] and contains a list of json strings. Extends {@link QbusMessageBase}.
+ * <p>
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+class QbusMessageListMap extends QbusMessageBase {
+
+    private List<Map<String, String>> outputs = new ArrayList<>();
+
+    List<Map<String, String>> getOutputs() {
+        return this.outputs;
+    }
+
+    void setOutputs(List<Map<String, String>> outputs) {
+        this.outputs = outputs;
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java
new file mode 100644 (file)
index 0000000..097d084
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.protocol;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Class {@link QbusMessageMap} used as output from gson for cmd or event feedback from Qbus where the
+ * data part is a simple json string. Extends {@link QbusMessageBase}.
+ * <p>
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+class QbusMessageMap extends QbusMessageBase {
+
+    private Map<String, String> outputs = new HashMap<>();
+
+    Map<String, String> getData() {
+        return this.outputs;
+    }
+
+    void setOutputs(Map<String, String> outputs) {
+        this.outputs = outputs;
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java
new file mode 100644 (file)
index 0000000..4358e7e
--- /dev/null
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.protocol;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.handler.QbusRolHandler;
+
+/**
+ * The {@link QbusRol} class represents the action Qbus Shutter/Slats output.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public final class QbusRol {
+
+    private @Nullable QbusCommunication qComm;
+
+    private Integer id;
+
+    private @Nullable Integer state;
+
+    private @Nullable Integer slats;
+
+    private @Nullable QbusRolHandler thingHandler;
+
+    QbusRol(Integer id) {
+        this.id = id;
+    }
+
+    /**
+     * This method should be called if the ThingHandler for the thing corresponding to this Shutter/Slats is
+     * initialized.
+     * It keeps a record of the thing handler in this object so the thing can be updated when
+     * the shutter/slat receives an update from the Qbus client.
+     *
+     * @param qbusRolHandler
+     */
+    public void setThingHandler(QbusRolHandler qbusRolHandler) {
+        this.thingHandler = qbusRolHandler;
+    }
+
+    /**
+     * This method sets a pointer to the qComm Shutter/Slats of class {@link QbusCommuncation}.
+     * This is then used to be able to call back the sendCommand method in this class to send a command to the
+     * Qbus IP-interface when..
+     *
+     * @param qComm
+     */
+    public void setQComm(QbusCommunication qComm) {
+        this.qComm = qComm;
+    }
+
+    /**
+     * Update the value of the Shutter.
+     *
+     * @param Shutter value
+     */
+    public void updateState(@Nullable Integer state) {
+        this.state = state;
+        QbusRolHandler handler = this.thingHandler;
+        if (handler != null) {
+            handler.handleStateUpdate(this);
+        }
+    }
+
+    /**
+     * Update the value of the Slats.
+     *
+     * @param Slat value
+     */
+    public void updateSlats(@Nullable Integer Slats) {
+        this.slats = Slats;
+        QbusRolHandler handler = this.thingHandler;
+        if (handler != null) {
+            handler.handleStateUpdate(this);
+        }
+    }
+
+    /**
+     * Get the value of the Shutter.
+     *
+     * @return shutter value
+     */
+    public @Nullable Integer getState() {
+        return this.state;
+    }
+
+    /**
+     * Get the value of the Slats.
+     *
+     * @return slats value
+     */
+    public @Nullable Integer getStateSlats() {
+        return this.slats;
+    }
+
+    /**
+     * Sends shutter state to Qbus.
+     *
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    public void execute(int value, String sn) throws InterruptedException, IOException {
+        QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeStore").withId(this.id).withState(value);
+        QbusCommunication comm = qComm;
+        if (comm != null) {
+            comm.sendMessage(qCmd);
+        }
+    }
+
+    /**
+     * Sends slats state to Qbus.
+     *
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    public void executeSlats(int value, String sn) throws InterruptedException, IOException {
+        QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeSlats").withId(this.id).withState(value);
+        QbusCommunication comm = qComm;
+        if (comm != null) {
+            comm.sendMessage(qCmd);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java
new file mode 100644 (file)
index 0000000..bc5fac0
--- /dev/null
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.protocol;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.handler.QbusSceneHandler;
+
+/**
+ * The {@link QbusScene} class represents the action Qbus Scene output.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public final class QbusScene {
+
+    private @Nullable QbusCommunication qComm;
+
+    public @Nullable QbusSceneHandler thingHandler;
+
+    private @Nullable Integer state;
+
+    private Integer id;
+
+    QbusScene(Integer id) {
+        this.id = id;
+    }
+
+    /**
+     * This method should be called if the ThingHandler for the thing corresponding to this scene is initialized.
+     * It keeps a record of the thing handler in this object so the thing can be updated when
+     * the scene output receives an update from the Qbus client.
+     *
+     * @param handler
+     */
+    public void setThingHandler(QbusSceneHandler handler) {
+        this.thingHandler = handler;
+    }
+
+    /**
+     * This method sets a pointer to the qComm SCENE of class {@link QbusCommuncation}.
+     * This is then used to be able to call back the sendCommand method in this class to send a command to the
+     * Qbus client.
+     *
+     * @param qComm
+     */
+    public void setQComm(QbusCommunication qComm) {
+        this.qComm = qComm;
+    }
+
+    /**
+     * Get the value of the Scene.
+     *
+     * @return Scene value
+     */
+    public @Nullable Integer getState() {
+        return this.state;
+    }
+
+    /**
+     * Sends Scene state to Qbus.
+     * 
+     * @param value
+     * @param sn
+     * @throws InterruptedException
+     * @throws IOException
+     */
+    public void execute(int value, String sn) throws InterruptedException, IOException {
+        QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeScene").withId(this.id).withState(value);
+        QbusCommunication comm = qComm;
+        if (comm != null) {
+            comm.sendMessage(qCmd);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java
new file mode 100644 (file)
index 0000000..493d6b6
--- /dev/null
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.qbus.internal.protocol;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler;
+
+/**
+ * The {@link QbusThermostat} class represents the thermostat Qbus communication object. It contains all
+ * fields representing a Qbus thermostat and has methods to set the thermostat mode and setpoint in Qbus and
+ * receive thermostat updates.
+ *
+ * @author Koen Schockaert - Initial Contribution
+ */
+
+@NonNullByDefault
+public final class QbusThermostat {
+
+    private @Nullable QbusCommunication qComm;
+
+    private Integer id;
+    private double measured = 0.0;
+    private double setpoint = 0.0;
+    private @Nullable Integer mode;
+
+    private @Nullable QbusThermostatHandler thingHandler;
+
+    QbusThermostat(Integer id) {
+        this.id = id;
+    }
+
+    /**
+     * This method should be called if the ThingHandler for the thing corresponding to the termostat is initialized.
+     * It keeps a record of the thing handler in this object so the thing can be updated when
+     * the thermostat receives an update from the Qbus client.
+     *
+     * @param handler
+     */
+    public void setThingHandler(QbusThermostatHandler handler) {
+        this.thingHandler = handler;
+    }
+
+    /**
+     * This method sets a pointer to the qComm THERMOSTAT of class {@link QbusCommuncation}.
+     * This is then used to be able to call back the sendCommand method in this class to send a command to the
+     * Qbus client.
+     *
+     * @param qComm
+     */
+    public void setQComm(QbusCommunication qComm) {
+        this.qComm = qComm;
+    }
+
+    /**
+     * Update all values of the Thermostat
+     *
+     * @param measured current temperature in 1°C multiples
+     * @param setpoint the setpoint temperature in 1°C multiples
+     * @param mode 0="Manual", 1="Freeze", 2="Economic", 3="Comfort", 4="Night"
+     */
+    public void updateState(Double measured, Double setpoint, Integer mode) {
+        this.measured = measured;
+        this.setpoint = setpoint;
+        this.mode = mode;
+
+        QbusThermostatHandler handler = this.thingHandler;
+        if (handler != null) {
+            handler.handleStateUpdate(this);
+        }
+    }
+
+    /**
+     * Get measured temperature of the Thermostat.
+     *
+     * @return measured temperature in 0.5°C multiples
+     */
+    public @Nullable Double getMeasured() {
+        return this.measured;
+    }
+
+    /**
+     * Get setpoint temperature of the Thermostat.
+     *
+     * @return the setpoint temperature in 0.5°C multiples
+     */
+    public @Nullable Double getSetpoint() {
+        return this.setpoint;
+    }
+
+    /**
+     * Get the Thermostat mode.
+     *
+     * @return the mode: 0="Manual", 1="Freeze", 2="Economic", 3="Comfort", 4="Night"
+     */
+    public @Nullable Integer getMode() {
+        return this.mode;
+    }
+
+    /**
+     * Sends Thermostat mode to Qbus.
+     *
+     * @param mode
+     * @param sn
+     * @throws InterruptedException
+     * @throws IOException
+     */
+    public void executeMode(int mode, String sn) throws InterruptedException, IOException {
+        QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withMode(mode);
+        QbusCommunication comm = this.qComm;
+        if (comm != null) {
+            comm.sendMessage(qCmd);
+        }
+    }
+
+    /**
+     * Sends Thermostat setpoint to Qbus.
+     *
+     * @param setpoint
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    public void executeSetpoint(double setpoint, String sn) throws InterruptedException, IOException {
+        QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withSetPoint(setpoint);
+        QbusCommunication comm = this.qComm;
+        if (comm != null) {
+            comm.sendMessage(qCmd);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..2457b9d
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="qbus" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>Qbus Binding</name>
+       <description>This is the binding for the Qbus home automation system. Qbus is a system made and developed in Belgium
+               (https://www.qbus.be/nl-nl)</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties
new file mode 100644 (file)
index 0000000..c1e57f9
--- /dev/null
@@ -0,0 +1,85 @@
+# binding
+binding.qbus.name = Qbus Binding
+binding.qbus.description = Deze binding maakt via een server applicate verbinding met de Qbus controller.
+
+# thing types
+thing-type.qbus.bridge.label = Qbus Bridge
+thing-type.qbus.bridge.description = De Qbus Bridge Maakt Verbinding Met de Qbus Server.
+thing-type.config.qbus.bridge.addr.label = IP Adres of Host Naam
+thing-type.config.qbus.bridge.addr.description = IP adres van de Qbus server, meestal 'localhost'
+thing-type.config.qbus.bridge.sn.label = Serienummer van de Controller
+thing-type.config.qbus.bridge.sn.description = Serienummer van de CTD controller
+thing-type.config.qbus.bridge.port.label = Poort
+thing-type.config.qbus.bridge.port.description = Communicatiepoort van de Qbus server (standaard: 8447)
+thing-type.config.qbus.bridge.serverCheck.label = Server Connectie
+thing-type.config.qbus.bridge.serverCheck.description = Ingestelde timer, bij het verlopen van de timer zal de communicatie met de Qbus server gecontroleerd worden en indien nodig herstart.
+
+thing-type.qbus.onOff.label = Aan/uit
+thing-type.qbus.onOff.description = Alle Bistabiel-Mono-Timer-Interval uitgangen
+thing-type.config.qbus.onOff.bistabielId.label = Qbus ID
+thing-type.config.qbus.onOff.bistabielId.description = Identificatienummer van de uitgang (zie SMIII)
+
+thing-type.qbus.scene.label = Sfeer
+thing-type.qbus.scene.description = Alle sferen
+thing-type.config.qbus.scene.sceneId.label = Qbus ID
+thing-type.config.qbus.scene.sceneId.description = Identificatienummer van de sfeer (zie SMIII)
+
+thing-type.qbus.co2.label = CO2
+thing-type.qbus.co2.description = Alle CO2 Uitgangen
+thing-type.config.qbus.co2.co2Id.label = Qbus ID
+thing-type.config.qbus.co2.co2Id.description = Identificatienummer van de uitgang (zie SMIII)
+
+thing-type.qbus.dimmer.label = Dimmer
+thing-type.qbus.dimmer.description = Alle Dimbare Uitgangen
+thing-type.config.qbus.dimmer.dimmerId.label = Qbus ID
+thing-type.config.qbus.dimmer.dimmerId.description = Identificatienummer van de uitgang (zie SMIII)
+thing-type.config.qbus.dimmer.step.label = Stappenwaarde
+thing-type.config.qbus.dimmer.step.description = Waarde gebruikt voor het dimmen in stappen (standaard 10%)
+
+thing-type.qbus.rollershutter.label = Rolluik
+thing-type.qbus.rollershutter.description = Alle Rolluik (ROL02P) Uitgangen
+thing-type.config.qbus.rollershutter.rolId.label = Qbus ID
+thing-type.config.qbus.rollershutter.rolId.description = Identificatienummer van de uitgang (zie SMIII)
+
+thing-type.qbus.rollershutter_slats.label = Rolluik (met lamellen)
+thing-type.qbus.rollershutter_slats.description = Alle schermen met lamellen (ROL02P) uitgang
+thing-type.config.qbus.rollershutter_slats.rolId.label = Qbus ID
+thing-type.config.qbus.rollershutter_slats.rolId.description = Identificatienummer van de uitgang (zie SMIII)
+
+thing-type.qbus.thermostat.label = Thermostaat
+thing-type.qbus.thermostat.description = Alle thermostaten
+thing-type.config.qbus.thermostat.thermostatId.label = Qbus ID
+thing-type.config.qbus.thermostat.thermostatId.description = Identificatienummer van de uitgang (zie SMIII)
+
+channel-type.qbus.scene.label = Sfeer
+channel-type.qbus.scene.description = Bediening van de sfeer
+
+channel-type.qbus.co2.label = CO2
+channel-type.qbus.co2.description = Uitlezing van de CO2 waarde
+
+channel-type.qbus.switch.label = Schakelaar
+channel-type.qbus.switch.description = Schakelaar bediening van de uitgangen
+
+channel-type.qbus.brightness.label = Helderheid
+channel-type.qbus.brightness.description = Helderheid bediening van de uitgangen
+
+channel-type.qbus.measured.label = Gemeten Temperatuur
+channel-type.qbus.measured.description = Uitlezing van de gemeten Temperatuur
+
+channel-type.qbus.setpoint.label = Ingestelde Temperatuur
+channel-type.qbus.setpoint.description = Ingestelde temperatuur bediening van de uitgangen
+
+channel-type.qbus.mode.label = Ingesteld Regime
+channel-type.qbus.mode.description = Regime bediening van de uitgangen
+channel-type.qbus.mode.state.option.0 = Manueel
+channel-type.qbus.mode.state.option.1 = Vorst
+channel-type.qbus.mode.state.option.2 = Economisch
+channel-type.qbus.mode.state.option.3 = Comfort
+channel-type.qbus.mode.state.option.4 = Nacht
+
+channel-type.qbus.rollershutter.label = Rolluik Bediening
+channel-type.qbus.rollershutter.description = Rolluik bediening van de uitgangen
+
+channel-type.qbus.slats.label = Lamellen Bediening
+channel-type.qbus.slats.description = Lamellen bediening van de uitgangen
+
diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..0b8a00c
--- /dev/null
@@ -0,0 +1,231 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="qbus"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <bridge-type id="bridge">
+               <label>Qbus Bridge</label>
+               <description>This bridge represents a Qbus client</description>
+               <config-description>
+                       <parameter name="addr" type="text" required="true">
+                               <label>Hostname</label>
+                               <description>IP address or hostname of Qbus server, usually 'localhost'</description>
+                               <default>localhost</default>
+                               <context>network-address</context>
+                       </parameter>
+                       <parameter name="sn" type="text" required="true">
+                               <label>Serial Number</label>
+                               <description>Serial number of the CTD controller</description>
+                       </parameter>
+                       <parameter name="port" type="integer" required="false">
+                               <label>Bridge Port</label>
+                               <description>Port to communicate with Qbus server, default 8447</description>
+                               <default>8447</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="serverCheck" type="integer" required="false" unit="min" min="1">
+                               <label>Server Check</label>
+                               <description>Time to check communication with Qbus Server (min), default 10. If set to 0 or left empty, no refresh
+                                       will be scheduled</description>
+                               <default>10</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+       <thing-type id="onOff">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+               <label>Switch</label>
+               <description>Bistabiel-Mono-Timer-Interval Output</description>
+               <channels>
+                       <channel id="switch" typeId="system.power"/>
+               </channels>
+               <config-description>
+                       <parameter name="bistabielId" type="integer" required="true">
+                               <label>Qbus ID</label>
+                               <description>Qbus Bistabiel ID</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <thing-type id="scene">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+               <label>Scene</label>
+               <description>Qbus Scene</description>
+               <channels>
+                       <channel id="scene" typeId="scene"/>
+               </channels>
+               <config-description>
+                       <parameter name="sceneId" type="integer" required="true">
+                               <label>Qbus Scene ID</label>
+                               <description>Qbus Scene ID</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <thing-type id="co2">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+               <label>CO2</label>
+               <description>Qbus CO2</description>
+               <channels>
+                       <channel id="co2" typeId="co2"/>
+               </channels>
+               <config-description>
+                       <parameter name="co2Id" type="integer" required="true">
+                               <label>Qbus CO2 ID</label>
+                               <description>Qbus CO2 ID</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <thing-type id="dimmer">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+               <label>Dimmer</label>
+               <description>Qbus Dimmer Output</description>
+               <channels>
+                       <channel id="brightness" typeId="system.brightness"/>
+               </channels>
+               <config-description>
+                       <parameter name="dimmerId" type="integer" required="true">
+                               <label>Output ID</label>
+                               <description>Qbus Dimmer ID</description>
+                       </parameter>
+                       <parameter name="step" type="integer" required="true">
+                               <label>Step Value</label>
+                               <description>Step value used for increase/decrease of dimmer brightness, default 10%</description>
+                               <default>10</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <thing-type id="rollershutter">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+               <label>RollerShutter</label>
+               <description>Qbus shutter (ROL02P) control</description>
+               <channels>
+                       <channel id="rollershutter" typeId="rollershutter"/>
+               </channels>
+               <config-description>
+                       <parameter name="rolId" type="integer" required="true">
+                               <label>Rol ID</label>
+                               <description>Qbus Rol Id</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <thing-type id="rollershutter_slats">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+               <label>RollerShutter (With Slats)</label>
+               <description>Qbus shutter with slats control</description>
+               <channels>
+                       <channel id="rollershutter" typeId="rollershutter"/>
+                       <channel id="slats" typeId="slats"/>
+               </channels>
+               <config-description>
+                       <parameter name="rolId" type="integer" required="true">
+                               <label>Rol ID</label>
+                               <description>Qbus Rol Id</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <thing-type id="thermostat">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+               <label>Thermostat</label>
+               <description>Qbus Thermostat</description>
+               <channels>
+                       <channel id="measured" typeId="measured"/>
+                       <channel id="mode" typeId="mode"/>
+                       <channel id="setpoint" typeId="setpoint"/>
+               </channels>
+               <config-description>
+                       <parameter name="thermostatId" type="integer" required="true">
+                               <label>Thermostat ID</label>
+                               <description>Qbus Thermostat ID</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <channel-type id="scene">
+               <item-type>Switch</item-type>
+               <label>Scene</label>
+               <description>Scene Control for Qbus</description>
+               <category>Scene</category>
+       </channel-type>
+
+       <channel-type id="measured">
+               <item-type>Number:Temperature</item-type>
+               <label>Measured</label>
+               <description>Temperature Measured by Thermostat</description>
+               <category>Temperature</category>
+               <tags>
+                       <tag>CurrentTemperature</tag>
+               </tags>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="setpoint">
+               <item-type>Number:Temperature</item-type>
+               <label>Setpoint</label>
+               <description>Setpoint Temperature of Thermostat</description>
+               <category>Temperature</category>
+               <tags>
+                       <tag>TargetTemperature</tag>
+               </tags>
+               <state min="0" max="100" step="0.5" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="mode">
+               <item-type>Number</item-type>
+               <label>Mode</label>
+               <description>Thermostat Mode</description>
+               <category>Number</category>
+               <state>
+                       <options>
+                               <option value="0">Manual</option>
+                               <option value="1">Freeze</option>
+                               <option value="2">Economic</option>
+                               <option value="3">Comfort</option>
+                               <option value="4">Night</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="co2">
+               <item-type>Number</item-type>
+               <label>CO2</label>
+               <description>CO2 value for Qbus</description>
+               <category>CO2</category>
+       </channel-type>
+
+       <channel-type id="rollershutter">
+               <item-type>Rollershutter</item-type>
+               <label>Rollershutter</label>
+               <description>Rollershutter Control for Qbus</description>
+               <category>Blinds</category>
+       </channel-type>
+
+       <channel-type id="slats">
+               <item-type>Dimmer</item-type>
+               <label>Slatcontrol</label>
+               <description>Slatcontrol for Qbus</description>
+               <category>Blinds</category>
+       </channel-type>
+
+</thing:thing-descriptions>
index 38bfdb58ebe89a14418e404c1468c65bc679432d..e663fb9bec45b66fecaccd51bcabefdb8fc4c9e7 100644 (file)
     <module>org.openhab.binding.pulseaudio</module>
     <module>org.openhab.binding.pushbullet</module>
     <module>org.openhab.binding.pushover</module>
+    <module>org.openhab.binding.qbus</module>
     <module>org.openhab.binding.radiothermostat</module>
     <module>org.openhab.binding.regoheatpump</module>
     <module>org.openhab.binding.revogi</module>