diff --git a/bundles/org.openhab.binding.airq/NOTICE b/bundles/org.openhab.binding.airq/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.airq/NOTICE
@@ -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.airq/README.md b/bundles/org.openhab.binding.airq/README.md
new file mode 100644
index 0000000000000..2ef0e298ba859
--- /dev/null
+++ b/bundles/org.openhab.binding.airq/README.md
@@ -0,0 +1,229 @@
+# airq Binding
+
+The airq Binding integrates the air analyzer air-Q device into the openHAB system.
+
+With the binding, it is possible to subscribe to all data delivered by the air-Q device.
+
+
+
+## Supported Things
+
+One only Thing is supported: the air-Q device.
+This Binding was tested with an air-Q Pro device with 14 sensors. It should also work with an air-Q device with 11 sensors, but it was not tested yet.
+
+## Discovery
+
+Auto-discovery is not possible in this version. Since the binding has to be configured at least with the password of the device, auto-discovery would be of limited value anyway.
+
+## Binding Configuration
+
+The binding does not need to be configured.
+
+## Thing Configuration
+
+The air-Q thing must be configured with (both mandatory):
+| Parameter | Description |
+|-----------|------------------------------------|
+| ipAddress | Network address, e.g. 192.168.0.68 |
+| password | Password of the air-Q device |
+
+The Thing provides the following properties:
+| Parameter | Description |
+|------------------------|-------------------------------|
+| id | Device ID |
+| air-Q-Hardware-Version | Hardware version |
+| air-Q-Software-Version | Firmware version |
+| sensors | Available sensors |
+| SensorInfo | Information about the sensors |
+| Industry | Industry version |
+
+## Channels
+
+The air-Q Thing offers access to all sensor data of the air-Q, according to its version. This includes also the Maximum Error per sensor value.
+
+| channel | type | description |
+|---------------------------|----------|---------------------------------------------------------------------|
+| DeviceID | String | Individual ID of the device |
+| Status | String | Status of the sensors |
+| TypPS | Number | Average size of Fine Dust [experimental] |
+| bat | Number | Battery State |
+| cnt0_3 | Number | Fine Dust >0,3 μm |
+| cnt0_5 | Number | Fine Dust >0,5 μm |
+| cnt1 | Number | Fine Dust >1 μm |
+| cnt2_5 | Number | Fine Dust >2,5 μm |
+| cnt5 | Number | Fine Dust >5 μm |
+| cnt10 | Number | Fine Dust >10 μm |
+| co2 | Number | CO2 concentration |
+| dCO2dt | Number | Change of CO2 concentration |
+| dHdt | Number | Change of Humidity |
+| dewpt | Number | Dew Point |
+| door_event | Switch | Door Event (experimental) |
+| health | Number | Health Index |
+| humidity | Number | Humidity in percent |
+| humidity_abs | Number | Absolute Humidity |
+| measuretime | Number | Milliseconds needed for measurement |
+| no2 | Number | NO2 concentration |
+| o3 | Number | O3 concentration |
+| oxygen | Number | Oxygen concentration |
+| performance | Number | Performance index |
+| pm1 | Number | Fine Dust concentration >1 μm |
+| pm2_5 | Number | Fine Dust concentration >2.5 μm |
+| pm10 | Number | Fine Dust concentration >10 μm |
+| pressure | Number | Pressure |
+| so2 | Number | SO2 concentration |
+| sound | Number | Noise |
+| temperature | Number | Temperature |
+| timestamp | Time | Timestamp of measurement |
+| tvoc | Number | VOC concentration |
+| uptime | Number | uptime in seconds |
+| Wifi | Switch | WLAN on or off |
+| WLANssid | String | WLAN SSID |
+| pass | String | Device Password |
+| WifiInfo | Switch | Show WLAN status with LED |
+| TimeServer | String | Name of Timeserver address |
+| geopos | Location | Location of air-Q device |
+| nightmode_StartDay | String | Time to start day operation |
+| nightmode_StartNight | String | End of day operation |
+| nightmode_BrightnessDay | Number | Brightness of LED during the day |
+| nightmode_BrightnessNight | Number | Brightness of LED at night |
+| nightmode_FanNightOff | Switch | Switch off fan at night |
+| nightmode_WifiNightOff | Switch | Switch off WLAN at night |
+| devicename | String | Device Name |
+| RoomType | String | Type of room |
+| Logging | String | Logging level |
+| DeleteKey | String | Settings to be deleted |
+| FireAlarm | Switch | Send Fire Alarm if certain levels are met |
+| WLAN_config_Gateway | String | Network Gateway |
+| WLAN_config_MAC | String | MAC Address |
+| WLAN_config_SSID | String | WLAN SSID |
+| WLAN_config_IPAddress | String | Assigned IP address |
+| WLAN_config_NetMask | String | Network mask |
+| WLAN_config_BSSID | String | Network BSSID |
+| cloudUpload | Switch | Upload to air-q cloud |
+| SecondsMeasurementDelay | Number | Rhythm of measurement for historic average |
+| Rejection | String | Power Frequency |
+| AutoDriftCompensation | Switch | Compensate automatic drift |
+| AutoUpdate | Switch | Install Firmware updates automatically |
+| AdvancedDataProcessing | Switch | Use advanced algorithms eg. for open window or presence of a person |
+| ppm_and_ppb | Switch | Output CO as ppm and NO2, O3 and SO2 as ppb value instead of mg/m3 |
+| GasAlarm | Switch | Send Gas Alarm if certain levels are met |
+| id | String | Device ID, retrieved from configuration |
+| SoundInfo | Switch | Sound Info |
+| AlarmForwarding | Switch | Forward gas or fire alarm to other air-Q devices in the household |
+| usercalib | String | Last sensor calibration |
+| InitialCalFinished | Switch | Initial calibration has finished |
+| Averaging | Switch | Do an average |
+| ErrorBars | Switch | Calculate Maximum Errors |
+
+## Example
+
+### airq.things
+
+```
+Thing airq:airq:1 "air-Q" [ ipAddress="192.168.0.68", password="myAirQPassword" ]
+```
+
+### airq.items
+
+```
+String airQ_DeviceID "Device ID, retrieved from Data" {channel="airq:airq:1:DeviceID"}
+String airQ_Status "Status of Sensors" {channel="airq:airq:1:Status"}
+Number airQ_TypPS "Average" {channel="airq:airq:1:TypPS"}
+Number airQ_bat "Battery State" {channel="airq:airq:1:bat"}
+Number airQ_cnt03 "Fine Dust >0,3 µm" {channel="airq:airq:1:cnt0_3"}
+Number airQ_cnt05 "Fine Dust >0,5 µm" {channel="airq:airq:1:cnt0_5"}
+Number airQ_cnt1 "Fine Dust >1,0 µm" {channel="airq:airq:1:cnt1"}
+Number airQ_cnt25 "Fine Dust >2,5 µm" {channel="airq:airq:1:cnt2_5"}
+Number airQ_cnt5 "Fine Dust >5 µm" {channel="airq:airq:1:cnt5"}
+Number airQ_cnt10 "Fine Dust >10 µm" {channel="airq:airq:1:cnt10"}
+Number airQ_co2 "CO2 Concentration" {channel="airq:airq:1:co2"}
+Number airQ_dCO2dt "Change of CO2 Concentration" {channel="airq:airq:1:dCO2dt"}
+Number airQ_dHdt "Change of Humidity" {channel="airq:airq:1:dHdt"}
+Number:Temperature airQ_dewpt "Dew Point" {channel="airq:airq:1:dewpt"}
+Switch airQ_door_event "Door Event (exp.)" {channel="airq:airq:1:door_event"}
+Number airQ_health "Health Index" {channel="airq:airq:1:health"}
+Number airQ_humidity "Humidity" {channel="airq:airq:1:humidity"}
+Number airQ_humidity_abs "Absolute Humidity" {channel="airq:airq:1:humidity_abs"}
+Number airQ_measuretime "Time needed for measurement" {channel="airq:airq:1:measuretime"}
+Number airQ_no2 "NO2 concentration" {channel="airq:airq:1:no2"}
+Number airQ_o3 "O3 concentration" {channel="airq:airq:1:o3"}
+Number:Dimensionless airQ_oxygen "Oxygen concentration" {channel="airq:airq:1:oxygen"}
+Number airQ_performance "Performance Index" {channel="airq:airq:1:performance"}
+Number airQ_pm1 "Fine Dust Concentration >1µ" {channel="airq:airq:1:pm1"}
+Number airQ_pm2_5 "Fine Dust Concentration >2.5µ" {channel="airq:airq:1:pm2_5"}
+Number airQ_pm10 "Fine Dust Concentration >10µ" {channel="airq:airq:1:pm10"}
+Number airQ_pressure "Pressure" {channel="airq:airq:1:pressure"}
+Number airQ_so2 "SO2 concentration" {channel="airq:airq:1:so2"}
+Number airQ_sound "Noise" {channel="airq:airq:1:sound"}
+Number:Temperature airQ_temperature "Temperature" {channel="airq:airq:1:temperature"}
+DateTime airQ_timestamp "Time stamp" {channel="airq:airq:1:timestamp"}
+Number airQ_tvoc "VOC concentration" {channel="airq:airq:1:tvoc"}
+Number airQ_uptime "Uptime" {channel="airq:airq:1:uptime"}
+
+Number airQ_bat_maxerr "'Maximum error' of Battery State, second value" {channel="airq:airq:1:bat_maxerr"}
+Number airQ_cnt03_maxerr "Maximum error of Fine Dust >0,3 µm" {channel="airq:airq:1:cnt0_3_maxerr"}
+Number airQ_cnt05_maxerr "Maximum error of Fine Dust >0,5 µm" {channel="airq:airq:1:cnt0_5_maxerr"}
+Number airQ_cnt1_maxerr "Maximum error of Fine Dust >1,0 µm" {channel="airq:airq:1:cnt1_maxerr"}
+Number airQ_cnt25_maxerr "Maximum error of Fine Dust >2,5 µm" {channel="airq:airq:1:cnt2_5_maxerr"}
+Number airQ_cnt5_maxerr "Maximum error of Fine Dust >5 µm" {channel="airq:airq:1:cnt5_maxerr"}
+Number airQ_cnt10_maxerr "Maximum error of Fine Dust >10 µm" {channel="airq:airq:1:cnt10_maxerr"}
+Number airQ_co2_maxerr "Maximum error of CO2 Concentration" {channel="airq:airq:1:co2_maxerr"}
+Number:Temperature airQ_dewpt_maxerr "Maximum error of Dew Point" {channel="airq:airq:1:dewpt_maxerr"}
+Number airQ_humidity_maxerr "Maximum error of Humidity" {channel="airq:airq:1:humidity_maxerr"}
+Number airQ_humidity_abs_maxerr "Maximum error of Absolute Humidity" {channel="airq:airq:1:humidity_abs_maxerr"}
+Number airQ_no2_maxerr "Maximum error of NO2 concentration" {channel="airq:airq:1:no2_maxerr"}
+Number airQ_o3_maxerr "Maximum error of O3 concentration" {channel="airq:airq:1:o3_maxerr"}
+Number:Dimensionless airQ_oxygen_maxerr "Maximum error of Oxygen concentration" {channel="airq:airq:1:oxygen_maxerr"}
+Number airQ_pm1_maxerr "Maximum error of Fine Dust Concentration >1µ" {channel="airq:airq:1:pm1_maxerr"}
+Number airQ_pm2_5_maxerr "Maximum error of Fine Dust Concentration >2.5µ" {channel="airq:airq:1:pm2_5_maxerr"}
+Number airQ_pm10_maxerr "Maximum error of Fine Dust Concentration >10µ" {channel="airq:airq:1:pm10_maxerr"}
+Number airQ_pressure_maxerr "Maximum error of Pressure" {channel="airq:airq:1:pressure_maxerr"}
+Number airQ_so2_maxerr "Maximum error of SO2 concentration" {channel="airq:airq:1:so2_maxerr"}
+Number airQ_sound_maxerr "Maximum error of Noise" {channel="airq:airq:1:sound_maxerr"}
+Number:Temperature airQ_temperature_maxerr "Maximum error of Temperature" {channel="airq:airq:1:temperature_maxerr"}
+Number airQ_tvoc_maxerr "Maximum error of VOC concentration" {channel="airq:airq:1:tvoc_maxerr"}
+
+Switch airQ_Wifi "WLAN on or off" {channel="airq:airq:1:Wifi"}
+String airQ_WLANssid "WLAN SSID" {channel="airq:airq:1:WLANssid"}
+String airQ_pass "Device Password" {channel="airq:airq:1:pass"}
+Switch airQ_WifiInfo "Show WLAN status with LED" {channel="airq:airq:1:WifiInfo"}
+String airQ_TimeServer "Name of Timeserver address" {channel="airq:airq:1:TimeServer"}
+Location airQ_geopos "Location of air-Q device" {channel="airq:airq:1:geopos"}
+String airQ_nightmode_StartDay "Time to start day operation" {channel="airq:airq:1:nightmode_StartDay"}
+String airQ_nightmode_StartNight "End of day operation" {channel="airq:airq:1:nightmode_StartNight"}
+Number airQ_nightmode_BrightnessDay "Brightness of LED during the day" {channel="airq:airq:1:nightmode_BrightnessDay"}
+Number airQ_nightmode_BrightnessNight "Brightness of LED at night" {channel="airq:airq:1:nightmode_BrightnessNight"}
+Switch airQ_nightmode_FanNightOff "Switch off fan at night" {channel="airq:airq:1:nightmode_FanNightOff"}
+Switch airQ_nightmode_WifiNightOff "Switch off WLAN at night" {channel="airq:airq:1:nightmode_WifiNightOff"}
+String airQ_devicename "Device Name" {channel="airq:airq:1:devicename"}
+String airQ_RoomType "Type of room" {channel="airq:airq:1:RoomType"}
+String airQ_logging "Logging level" {channel="airq:airq:1:logging"}
+String airQ_DeleteKey "Settings to be deleted" {channel="airq:airq:1:DeleteKey"}
+Switch airQ_FireAlarm "Send Fire Alarm if certain levels are met" {channel="airq:airq:1:FireAlarm"}
+String airQ_Hardware-Version "Hardware Version" {channel="airq:airq:1:air-Q-Hardware-Version"}
+String airQ_WLAN_config_Gateway "Network Gateway" {channel="airq:airq:1:WLAN_config_Gateway"}
+String airQ_WLAN_config_MAC "MAC Address" {channel="airq:airq:1:WLAN_config_MAC"}
+String airQ_WLAN_config_SSID "WLAN SSID" {channel="airq:airq:1:WLAN_config_SSID"}
+String airQ_WLAN_config_IPAddress "Assigned IP address" {channel="airq:airq:1:WLAN_config_IPAddress"}
+String airQ_WLAN_config_NetMask "Network mask" {channel="airq:airq:1:WLAN_config_NetMask"}
+String airQ_WLAN_config_BSSID "Network BSSID" {channel="airq:airq:1:WLAN_config_BSSID"}
+Switch airQ_cloudUpload "Upload to air-q cloud" {channel="airq:airq:1:cloudUpload"}
+Number airQ_SecondsMeasurementDelay "Rhythm of measurement for historic average" {channel="airq:airq:1:SecondsMeasurementDelay"}
+String airQ_Rejection "Power Frequency" {channel="airq:airq:1:Rejection"}
+String airQ_Software-Version "Firmware version" {channel="airq:airq:1:air-Q-Software-Version"}
+String airQ_sensors "Available sensors" {channel="airq:airq:1:sensors"}
+Switch airQ_AutoDriftCompensation "Compensate automatic drift" {channel="airq:airq:1:AutoDriftCompensation"}
+Switch airQ_AutoUpdate "Install Firmware updates automatically" {channel="airq:airq:1:AutoUpdate"}
+Switch airQ_AdvancedDataProcessing "Use advanced algorithms eg. for open window or presence of a person" {channel="airq:airq:1:AdvancedDataProcessing"}
+Switch airQ_Industry "Industry Version" {channel="airq:airq:1:Industry"}
+Switch airQ_ppm_and_ppb "Output CO as ppm and NO2, O3 and SO2 as ppb value instead of mg/m3" {channel="airq:airq:1:ppm_and_ppb"}
+Switch airQ_GasAlarm "Send Gas Alarm if certain levels are met" {channel="airq:airq:1:GasAlarm"}
+String airQ_id "Device ID, retrieved from configuration" {channel="airq:airq:1:id"}
+Switch airQ_SoundInfo "Sound Info" {channel="airq:airq:1:SoundInfo"}
+Switch airQ_AlarmForwarding "Forward gas or fire alarm to other air-Q devices in the household" {channel="airq:airq:1:AlarmForwarding"}
+String airQ_usercalib "Last sensor calibration" {channel="airq:airq:1:usercalib"}
+Switch airQ_InitialCalFinished "Initial calibration has finished" {channel="airq:airq:1:InitialCalFinished"}
+Switch airQ_Averaging "Do an average" {channel="airq:airq:1:Averaging"}
+String airQ_SensorInfo "Information about the sensors" {channel="airq:airq:1:SensorInfo"}
+Switch airQ_ErrorBars "Calculate Maximum Errors" {channel="airq:airq:1:ErrorBars"}
+```
diff --git a/bundles/org.openhab.binding.airq/pom.xml b/bundles/org.openhab.binding.airq/pom.xml
new file mode 100644
index 0000000000000..152fc6cf2a307
--- /dev/null
+++ b/bundles/org.openhab.binding.airq/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.1.0-SNAPSHOT
+
+
+ org.openhab.binding.airq
+
+ openHAB Add-ons :: Bundles :: air-Q Binding
+
+
diff --git a/bundles/org.openhab.binding.airq/src/main/feature/feature.xml b/bundles/org.openhab.binding.airq/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..711cb5bc45363
--- /dev/null
+++ b/bundles/org.openhab.binding.airq/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.airq/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/airqBindingConstants.java b/bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/airqBindingConstants.java
new file mode 100644
index 0000000000000..b2533c0c876bc
--- /dev/null
+++ b/bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/airqBindingConstants.java
@@ -0,0 +1,35 @@
+/**
+ * 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.airq.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link airqBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Aurelio Caliaro - Initial contribution
+ */
+@NonNullByDefault
+public class airqBindingConstants {
+
+ private static final String BINDING_ID = "airq";
+
+ // List of all Thing Type UIDs
+ // public static final ThingTypeUID THING_TYPE_SAMPLE = new ThingTypeUID(BINDING_ID, "sample");
+ public static final ThingTypeUID THING_TYPE_AIRQ = new ThingTypeUID(BINDING_ID, "airq");
+
+ // List of all Channel ids
+ public static final String CHANNEL_1 = "channel1";
+}
diff --git a/bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/airqConfiguration.java b/bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/airqConfiguration.java
new file mode 100644
index 0000000000000..4c7a46743df70
--- /dev/null
+++ b/bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/airqConfiguration.java
@@ -0,0 +1,28 @@
+/**
+ * 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.airq.internal;
+
+/**
+ * The {@link airqConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Aurelio Caliaro - Initial contribution
+ */
+
+public class airqConfiguration {
+
+ /**
+ * Sample configuration parameter. Replace with your own.
+ */
+ public String ipaddress;
+ public String password;
+}
diff --git a/bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/airqHandler.java b/bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/airqHandler.java
new file mode 100644
index 0000000000000..fbaa14eff1f09
--- /dev/null
+++ b/bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/airqHandler.java
@@ -0,0 +1,1007 @@
+/**
+ * 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.airq.internal;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.text.SimpleDateFormat;
+import java.time.LocalTime;
+import java.time.format.DateTimeParseException;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.StringType;
+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.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+/**
+ * The {@link airqHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Aurelio Caliaro - Initial contribution
+ */
+@NonNullByDefault
+public class airqHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(airqHandler.class);
+ private @Nullable ScheduledFuture> pollingJob;
+ private @Nullable ScheduledFuture> getConfigDataJob;
+ private String ipaddress;
+ private String password;
+ private @Nullable ThingStatus thStatus;
+ protected static final int POLLING_PERIOD_DATA = 15000; // in milliseconds
+ protected static final int POLLING_PERIOD_CONFIG = 1; // in minutes
+
+ final class ResultPair {
+ private final float value;
+ private final float maxdev;
+
+ public float getvalue() {
+ return value;
+ }
+
+ public float getmaxdev() {
+ return maxdev;
+ }
+
+ // ResultPair() expects a string formed as this: [1234,56,789,012] and gives back a ResultPair
+ // consisting of the two numbers
+ public ResultPair(String input) {
+ value = Float.parseFloat(input.substring(1, input.indexOf(',')));
+ maxdev = Float.parseFloat(input.substring(input.indexOf(',') + 1, input.length() - 1));
+ // value = new Float(input.substring(1, input.indexOf(',')));
+ // maxdev = new Float(input.substring(input.indexOf(',') + 1, input.length() - 1));
+ }
+ }
+
+ public airqHandler(Thing thing) {
+ super(thing);
+ ipaddress = "";
+ password = "";
+ }
+
+ public Boolean ohCmd2airqCmd(String ohcmd) {
+ switch (ohcmd) {
+ case "ON":
+ return true;
+ case "OFF":
+ return false;
+ }
+ return false;
+ }
+
+ private boolean isTimeFormat(String str) {
+ try {
+ LocalTime.parse(str);
+ } catch (DateTimeParseException e) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.info(
+ "air-Q - airqHandler - handleCommand(): request received to handle value {} command {} of channelUID={}",
+ command, command.getClass(), channelUID);
+ if ((command instanceof OnOffType) || (command instanceof StringType)) {
+ JsonObject newobj = new JsonObject();
+ JsonObject subjson = new JsonObject();
+ switch (channelUID.getId()) {
+ case "Wifi":
+ break; // we do not allow to switch off Wifi because otherwise we can't connect to the air-Q device
+ // anymore
+ case "WifiInfo":
+ case "FireAlarm":
+ case "cloudUpload":
+ case "AutoDriftCompensation":
+ case "AutoUpdate":
+ case "AdvancedDataProcessing":
+ case "GasAlarm":
+ case "SoundInfo":
+ case "AlarmForwarding":
+ case "Averaging":
+ case "ErrorBars":
+ newobj.addProperty(channelUID.getId(), ohCmd2airqCmd(command.toString()));
+ changeSettings(newobj);
+ break;
+ case "getHistoryFiles":
+ getDataFiles();
+ break;
+ case "ppm_and_ppb":
+ newobj.addProperty("ppm&ppb", ohCmd2airqCmd(command.toString()));
+ changeSettings(newobj);
+ case "nightmode_FanNightOff":
+ subjson.addProperty("FanNightOff", ohCmd2airqCmd(command.toString()));
+ newobj.add("NightMode", subjson);
+ changeSettings(newobj);
+ break;
+ case "nightmode_WifiNightOff":
+ subjson.addProperty("WifiNightOff", ohCmd2airqCmd(command.toString()));
+ newobj.add("NightMode", subjson);
+ changeSettings(newobj);
+ break;
+ case "airQ_nightmode_StartNight":
+ JsonElement nightmodeel = new Gson().fromJson(command.toString(), JsonElement.class);
+ if (nightmodeel != null) {
+ JsonObject nightmodeobj = nightmodeel.getAsJsonObject().getAsJsonObject("NightMode");
+ logger.info("nightmodeobj={}", nightmodeobj);
+ newobj.addProperty("StartDay", nightmodeobj.get("StartDay").getAsString());
+ newobj.addProperty("StartNight", nightmodeobj.get("StartNight").getAsString());
+ newobj.addProperty("BrightnessDay", nightmodeobj.get("BrightnessDay").getAsFloat());
+ newobj.addProperty("BrightnessNight", nightmodeobj.get("BrightnessNight").getAsFloat());
+ newobj.addProperty("FanNightOff", nightmodeobj.get("FanNightOff").getAsBoolean());
+ newobj.addProperty("WifiNightOff", nightmodeobj.get("WifiNightOff").getAsBoolean());
+ logger.info("air-Q - airqHandler - handleCommand(): dummy to change settings: {}",
+ newobj.toString());
+ } else {
+ logger.error("Cannot extract nightmode data from this string: {}", nightmodeel);
+ }
+ // changeSettings(newobj);
+ break;
+ case "WLANssid":
+ JsonElement wifidatael = new Gson().fromJson(command.toString(), JsonElement.class);
+ if (wifidatael != null) {
+ JsonObject wifidataobj = wifidatael.getAsJsonObject();
+ newobj.addProperty("WiFissid", wifidataobj.get("WiFissid").getAsString());
+ newobj.addProperty("WiFipass", wifidataobj.get("WiFipass").getAsString());
+ String bssid = wifidataobj.get("WiFibssid").getAsString();
+ if (!bssid.equals("")) {
+ newobj.addProperty("WiFibssid", bssid);
+ }
+ newobj.addProperty("reset", wifidataobj.get("reset").getAsString());
+ logger.info("air-Q - airqHandler - handleCommand(): dummy to change settings: {}",
+ newobj.toString());
+ changeSettings(newobj);
+ } else {
+ logger.error("Cannot extract wlan data from this string: {}", wifidatael);
+ }
+ break;
+ case "TimeServer":
+ newobj.addProperty(channelUID.getId(), command.toString());
+ changeSettings(newobj);
+ break;
+ case "nightmode_StartDay":
+ if (isTimeFormat(command.toString())) {
+ subjson.addProperty("StartDay", command.toString());
+ newobj.add("NightMode", subjson);
+ changeSettings(newobj);
+ } else {
+ logger.error(
+ "air-Q - airqHandler - handleCommand(): {} should be set to {} but it isn't a correct time format (eg. 08:00)",
+ channelUID.getId(), command.toString());
+ }
+ break;
+ case "nightmode_StartNight":
+ if (isTimeFormat(command.toString())) {
+ subjson.addProperty("StartNight", command.toString());
+ newobj.add("NightMode", subjson);
+ changeSettings(newobj);
+ } else {
+ logger.error(
+ "air-Q - airqHandler - handleCommand(): {} should be set to {} but it isn't a correct time format (eg. 08:00)",
+ channelUID.getId(), command.toString());
+ }
+ break;
+ // TODO
+ case "geopos":
+ break;
+ case "nightmode_BrightnessDay":
+ break;
+ case "nightmode_BrightnessNight":
+ break;
+ case "RoomType":
+ break;
+ case "Logging":
+ break;
+ case "SecondsMeasurementDelay":
+ break;
+ case "Rejection":
+ break;
+ default:
+ logger.error(
+ "air-Q - airqHandler - handleCommand(): unknown command {} received (channelUID={}, value={})",
+ command, channelUID, command);
+ }
+ } else if (command instanceof RefreshType) {
+ if (pollingJob != null) {
+ // pollingJob.notify();
+ // TODO: handle data refresh
+ }
+ }
+
+ // TODO: handle command
+
+ // Note: if communication with thing fails for some reason,
+ // indicate that by setting the status with detail information:
+ // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ // "Could not control device at IP address x.x.x.x");
+ }
+
+ @Override
+ public void initialize() {
+ logger.debug("air-Q - airqHandler - initialize(): ipaddress={}, password={}",
+ getThing().getConfiguration().get("ipAddress"), getThing().getConfiguration().get("password"));
+ // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
+ // the framework is then able to reuse the resources from the thing handler initialization.
+ // we set this upfront to reliably check status updates in unit tests.
+ updateStatus(ThingStatus.UNKNOWN);
+ if (getThing().getConfiguration().get("ipAddress") != null) {
+ ipaddress = getThing().getConfiguration().get("ipAddress").toString();
+ }
+ if (getThing().getConfiguration().get("password") != null) {
+ password = getThing().getConfiguration().get("password").toString();
+ }
+ if ((ipaddress.equals("")) || (password.equals(""))) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "IP Address and the device password must be provided to access your air-Q.");
+ return;
+ } else {
+ // we try if the device is reachable and the password is correct. Otherwise a corresponding message is
+ // thrown in Thing manager.
+ String data = getDecryptedContentString("http://".concat(ipaddress.concat("/data")), "GET", null);
+ if (data == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "We tried to get data from the air-Q device, but failed. Maybe the password is wrong.");
+ }
+ /*
+ * try {
+ * Result testres = null;
+ * if (ipaddress != null) {
+ * testres = doNetwork("http://".concat(ipaddress.concat("/data")), "GET", null);
+ * }
+ * if (testres != null) {
+ * String jsontext = testres.getBody();
+ * Gson gson = new Gson();
+ * JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
+ * JsonObject jsonObj = ans.getAsJsonObject();
+ * // We don't actually use the result here, it is just to try if it doesn't throw an Exception that
+ * // shows a wrong password
+ * decrypt(jsonObj.get("content").getAsString().getBytes(),
+ * getThing().getConfiguration().get("password").toString());
+ * }
+ * } catch (Exception e) {
+ * System.out.println("air-Q - airqHandler - polldata.run(): Error while testing air-Q data retrieval: "
+ * + e.toString());
+ * updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ * "We tried to get data from the air-Q device, but failed. Maybe the password is wrong.");
+ * }
+ */
+ }
+
+ // TODO: Initialize the handler.
+ // The framework requires you to return from this method quickly. Also, before leaving this method a thing
+ // status from one of ONLINE, OFFLINE or UNKNOWN must be set. This might already be the real thing status in
+ // case you can decide it directly.
+ // In case you can not decide the thing status directly (e.g. for long running connection handshake using
+ // WAN
+ // access or similar) you should set status UNKNOWN here and then decide the real status asynchronously in
+ // the
+ // background.
+
+ // Example for background initialization:
+ scheduler.execute(() ->
+
+ {
+ boolean thingReachable = true; //
+ // when done do:
+ if (thingReachable) {
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.OFFLINE);
+ }
+ });
+
+ // The following code will be called regularly. We only have it here to test the function
+ // Gson code based on https://riptutorial.com/de/gson
+ Runnable pollData = new Runnable() {
+
+ @Override
+ public void run() {
+ logger.trace("air-Q - airqHandler - run(): starting polled data handler");
+ if ((!ipaddress.equals("")) && (!password.equals(""))) {
+ try {
+ String url = "http://".concat(ipaddress.concat("/data"));
+ String jsonAnswer = getDecryptedContentString(url, "GET", null);
+ if (jsonAnswer != null) {
+ Gson gson = new Gson();
+ JsonElement decEl = gson.fromJson(jsonAnswer, JsonElement.class);
+ if (decEl != null) {
+ JsonObject decObj = decEl.getAsJsonObject();
+ logger.trace("air-Q - airqHandler - run(): decObj={}", decObj);
+ processType(decObj, "bat", "bat", "pair");
+ processType(decObj, "cnt0_3", "cnt0_3", "pair");
+ processType(decObj, "cnt0_5", "cnt0_5", "pair");
+ processType(decObj, "cnt1", "cnt1", "pair");
+ processType(decObj, "cnt2_5", "cnt2_5", "pair");
+ processType(decObj, "cnt5", "cnt5", "pair");
+ processType(decObj, "cnt10", "cnt10", "pair");
+ processType(decObj, "co2", "co2", "pair");
+ processType(decObj, "dewpt", "dewpt", "pair");
+ processType(decObj, "humidity", "humidity", "pair");
+ processType(decObj, "humidity_abs", "humidity_abs", "pair");
+ processType(decObj, "no2", "no2", "pair");
+ processType(decObj, "o3", "o3", "pair");
+ processType(decObj, "oxygen", "oxygen", "pair");
+ processType(decObj, "pm1", "pm1", "pair");
+ processType(decObj, "pm2_5", "pm2_5", "pair");
+ processType(decObj, "pm10", "pm10", "pair");
+ processType(decObj, "pressure", "pressure", "pair");
+ processType(decObj, "so2", "so2", "pair");
+ processType(decObj, "sound", "sound", "pair");
+ processType(decObj, "temperature", "temperature", "pair");
+ /*
+ * We have two places where the Device ID is delivered: with the measurement data and
+ * with the configuration.
+ * We take the info from the configuration and show it as a property, so we don't need
+ * this at this moment. We
+ * leave this as a reminder in case for some reason it will be needed in future, e.g.
+ * when an air-Q device
+ * also sends data from other devices (then with another Device ID)
+ *
+ * processType(decObj, "DeviceID", "DeviceID", "string");
+ */
+ processType(decObj, "Status", "Status", "string");
+ processType(decObj, "TypPS", "TypPS", "number");
+ processType(decObj, "dCO2dt", "dCO2dt", "number");
+ processType(decObj, "dHdt", "dHdt", "number");
+ processType(decObj, "door_event", "door_event", "number");
+ processType(decObj, "health", "health", "number");
+ processType(decObj, "measuretime", "measuretime", "number");
+ processType(decObj, "performance", "performance", "number");
+ processType(decObj, "timestamp", "timestamp", "datetime");
+ processType(decObj, "uptime", "uptime", "number");
+ processType(decObj, "tvoc", "tvoc", "pair");
+ } else {
+ logger.error("The air-Q data could not be extracted from this string: {}", decEl);
+ }
+ }
+ } catch (Exception e) {
+ System.out.println("air-Q - airqHandler - polldata.run(): Error while retrieving air-Q data: "
+ + e.toString());
+ }
+ }
+ }
+ };
+
+ pollingJob = scheduler.scheduleWithFixedDelay(pollData, 0, POLLING_PERIOD_DATA, TimeUnit.MILLISECONDS);
+ getConfigDataJob = scheduler.scheduleWithFixedDelay(getConfigData, 0, POLLING_PERIOD_CONFIG, TimeUnit.MINUTES);
+
+ // Note: When initialization can NOT be done set the status with more details for further
+ // analysis. See also class ThingStatusDetail for all available status details.
+ // Add a description to give user information to understand why thing does not work as expected. E.g.
+ // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ // "Can not access device as username and/or password are invalid");
+ logger.debug("air-Q - airqHandler - initialize() finished");
+ }
+
+ // AES decoding based on this tutorial: https://www.javainterviewpoint.com/aes-256-encryption-and-decryption/
+ public @Nullable String decrypt(byte[] base64text, String password) {
+ String content = "";
+ logger.trace("air-Q - airqHandler - decrypt(): password={}, content to decypt: {}", password, base64text);
+ byte[] encodedtextwithIV = Base64.getDecoder().decode(base64text);
+ byte[] ciphertext = Arrays.copyOfRange(encodedtextwithIV, 16, encodedtextwithIV.length);
+ byte[] passkey = Arrays.copyOf(password.getBytes(), 32);
+ if (password.length() < 32) {
+ Arrays.fill(passkey, password.length(), 32, (byte) '0');
+ }
+ byte[] IV = Arrays.copyOf(encodedtextwithIV, 16);
+ // logger.trace("air-Q - airqHandler - decrypt(): passkey={}", passkey);
+ // logger.trace("air-Q - airqHandler - decrypt(): IV={}", IV);
+ // logger.trace("air-Q - airqHandler - decrypt(): text to decode: {}", ciphertext);
+ SecretKey seckey = new SecretKeySpec(passkey, 0, passkey.length, "AES");
+ try {
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ SecretKeySpec keySpec = new SecretKeySpec(seckey.getEncoded(), "AES");
+ IvParameterSpec ivSpec = new IvParameterSpec(IV);
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+ byte[] decryptedText = cipher.doFinal(ciphertext);
+ content = new String(decryptedText);
+ logger.trace("air-Q - airqHandler - decrypt(): Text decoded as String: {}", content);
+ } catch (BadPaddingException bpe) {
+ System.out.println("Error while decrypting. Probably the provided password is wrong.");
+ return null;
+ } catch (Exception e) {
+ System.out.println("air-Q - airqHandler - decrypt(): Error while decrypting: " + e.toString());
+ return null;
+ }
+ return content;
+ }
+
+ public String encrypt(byte[] toencode, String password) {
+ String content = "";
+ logger.trace("air-Q - airqHandler - encrypt(): text to encode: {}", new String(toencode));
+ byte[] passkey = Arrays.copyOf(password.getBytes(), 32);
+ if (password.length() < 32) {
+ Arrays.fill(passkey, password.length(), 32, (byte) '0');
+ }
+ byte[] IV = new byte[16];
+ SecureRandom random = new SecureRandom();
+ random.nextBytes(IV);
+ SecretKey seckey = new SecretKeySpec(passkey, 0, passkey.length, "AES");
+ try {
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ SecretKeySpec keySpec = new SecretKeySpec(seckey.getEncoded(), "AES");
+ IvParameterSpec ivSpec = new IvParameterSpec(IV);
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+ byte[] encryptedText = cipher.doFinal(toencode);
+ byte[] totaltext = new byte[16 + encryptedText.length];
+ System.arraycopy(IV, 0, totaltext, 0, 16);
+ System.arraycopy(encryptedText, 0, totaltext, 16, encryptedText.length);
+ byte[] encodedcontent = Base64.getEncoder().encode(totaltext);
+ logger.trace("air-Q - airqHandler - encrypt(): encrypted text: {}", encodedcontent);
+ content = new String(encodedcontent);
+ // logger.debug("air-Q - airqHandler - encrypt(): content={}", content);
+ } catch (Exception e) {
+ System.out.println("air-Q - airqHandler - encrypt(): Error while encrypting: " + e.toString());
+ }
+ return content;
+ }
+
+ protected @Nullable String getDecryptedContentString(String url, String requestMethod, @Nullable String body) {
+ Result res = null;
+ String jsonAnswer = null;
+ res = getData(url, "GET", null);
+ if (res != null) {
+ String jsontext = res.getBody();
+ logger.trace("air-Q - airqHandler - getDecryptedContentString(): Result from doNetwork is {} with body={}",
+ res, res.getBody());
+ Gson gson = new Gson();
+ JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
+ if (ans != null) {
+ JsonObject jsonObj = ans.getAsJsonObject();
+ jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(),
+ (String) (getThing().getConfiguration().get("password")));
+ if (jsonAnswer == null) {
+ logger.error("The air-Q data could not be decrypted. Probably the password is wrong.");
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Wrong password");
+ }
+ } else {
+ logger.error("The air-Q data could not be extracted from this string: {}", ans);
+ }
+ }
+ return jsonAnswer;
+ }
+
+ // Do the networking job and in addition does additional tests for online/offline management
+ protected @Nullable Result getData(String address, String requestMethod, @Nullable String body) {
+ Result res = null;
+ res = doNetwork(address, "GET", null);
+ if (res == null) {
+ if (thStatus != ThingStatus.OFFLINE) {
+ logger.error("air-Q - airqHandler - run(): cannot reach air-Q device. Status set to OFFLINE.");
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "air-Q device not reachable");
+ thStatus = ThingStatus.OFFLINE;
+ } else {
+ logger.warn("air-Q - airqHandler - run(): retried but still cannot reach the air-Q device.");
+ }
+ } else {
+ if (thStatus == ThingStatus.OFFLINE) {
+ logger.error("air-Q - airqHandler - run(): can reach air-Q device again, Status set back to ONLINE.");
+ thStatus = ThingStatus.ONLINE;
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+ return res;
+ }
+
+ protected @Nullable Result doNetwork(String address, String requestMethod, @Nullable String body) {
+ int timeout = 10000;
+ HttpURLConnection conn = null;
+ logger.debug("air-Q - airqHandler - doNetwork(): connecting to {} with method {} and body {}", address,
+ requestMethod, body);
+ try {
+ conn = (HttpURLConnection) new URL(address).openConnection();
+ conn.setRequestMethod(requestMethod);
+ conn.setRequestProperty("Content-Type", "application/json");
+ conn.setConnectTimeout(timeout);
+ conn.setReadTimeout(timeout);
+ if (body != null && !"".equals(body)) {
+ conn.setDoOutput(true);
+ try (Writer out = new OutputStreamWriter(conn.getOutputStream())) {
+ out.write(body);
+ }
+ }
+ try (InputStream in = conn.getInputStream(); ByteArrayOutputStream result = new ByteArrayOutputStream()) {
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = in.read(buffer)) != -1) {
+ result.write(buffer, 0, length);
+ }
+ conn.disconnect();
+ return new Result(result.toString(StandardCharsets.UTF_8.name()), conn.getResponseCode());
+ }
+ } catch (IOException exc) {
+ System.out.println("air-Q - airqHandler - doNetwork(): Error while accessing air-Q: " + exc.toString());
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+ return null;
+ }
+
+ public static class Result {
+ private final String body;
+ private final int responseCode;
+
+ public Result(String body, int responseCode) {
+ this.body = body;
+ this.responseCode = responseCode;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public int getResponseCode() {
+ return responseCode;
+ }
+ }
+
+ @Override
+ public void dispose() {
+ if (pollingJob != null) {
+ pollingJob.cancel(true);
+ }
+ if (getConfigDataJob != null) {
+ getConfigDataJob.cancel(true);
+ }
+ }
+
+ Runnable getConfigData = new Runnable() {
+
+ @Override
+ public void run() {
+ Result res = null;
+ logger.trace("air-Q - airqHandler - processConfigData(): starting processing data");
+ if ((!ipaddress.equals("")) && (!password.equals(""))) {
+ try {
+ String url = "http://".concat(ipaddress.concat("/config"));
+ res = getData(url, "GET", null);
+ if (res != null) {
+ String jsontext = res.getBody();
+ logger.trace(
+ "air-Q - airqHandler - processConfigData(): Result from doNetwork is {} with body={}",
+ res, res.getBody());
+ Gson gson = new Gson();
+ JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
+ if (ans != null) {
+ JsonObject jsonObj = ans.getAsJsonObject();
+ String jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(),
+ (String) (getThing().getConfiguration().get("password")));
+ if (jsonAnswer != null) {
+ JsonElement decEl = gson.fromJson(jsonAnswer, JsonElement.class);
+ if (decEl != null) {
+ JsonObject decObj = decEl.getAsJsonObject();
+ logger.trace("air-Q - airqHandler - processConfigData(): decObj={}", decObj);
+ processType(decObj, "Wifi", "Wifi", "boolean");
+ processType(decObj, "WLANssid", "WLANssid", "arr");
+ processType(decObj, "pass", "pass", "string");
+ processType(decObj, "WifiInfo", "WifiInfo", "boolean");
+ processType(decObj, "TimeServer", "TimeServer", "string");
+ processType(decObj, "geopos", "geopos", "coord");
+ processType(decObj, "NightMode", "", "nightmode");
+ processType(decObj, "devicename", "devicename", "string");
+ processType(decObj, "RoomType", "RoomType", "string");
+ processType(decObj, "Logging", "Logging", "string");
+ processType(decObj, "DeleteKey", "DeleteKey", "string");
+ processType(decObj, "FireAlarm", "FireAlarm", "boolean");
+ processType(decObj, "air-Q-Hardware-Version", "air-Q-Hardware-Version", "property");
+ processType(decObj, "WLAN config", "", "wlan");
+ processType(decObj, "cloudUpload", "cloudUpload", "boolean");
+ processType(decObj, "SecondsMeasurementDelay", "SecondsMeasurementDelay", "number");
+ processType(decObj, "Rejection", "Rejection", "string");
+ processType(decObj, "air-Q-Software-Version", "air-Q-Software-Version", "property");
+ processType(decObj, "sensors", "sensors", "proparr");
+ processType(decObj, "AutoDriftCompensation", "AutoDriftCompensation", "boolean");
+ processType(decObj, "AutoUpdate", "AutoUpdate", "boolean");
+ processType(decObj, "AdvancedDataProcessing", "AdvancedDataProcessing", "boolean");
+ processType(decObj, "Industry", "Industry", "property");
+ processType(decObj, "ppm&ppb", "ppm_and_ppb", "boolean");
+ processType(decObj, "GasAlarm", "GasAlarm", "boolean");
+ processType(decObj, "id", "id", "property");
+ processType(decObj, "SoundInfo", "SoundInfo", "boolean");
+ processType(decObj, "AlarmForwarding", "AlarmForwarding", "boolean");
+ processType(decObj, "usercalib", "usercalib", "calib");
+ processType(decObj, "InitialCalFinished", "InitialCalFinished", "boolean");
+ processType(decObj, "Averaging", "Averaging", "boolean");
+ processType(decObj, "SensorInfo", "SensorInfo", "property");
+ processType(decObj, "ErrorBars", "ErrorBars", "boolean");
+ } else {
+ logger.error("The air-Q data could not be extracted from this string: {}", decEl);
+ }
+ }
+ } else {
+ logger.error("The air-Q data could not be extracted from this string: {}", ans);
+ }
+ }
+ } catch (Exception e) {
+ System.out.println("Error in processConfigData(): " + e.toString());
+ }
+ }
+ }
+ };
+
+ private void processType(JsonObject dec, String airqName, String channelName, String type) {
+ logger.trace("air-Q - airqHandler - processType(): airqName={}, channelName={}, type={}, dec={}", airqName,
+ channelName, type, dec);
+ if (dec.get(airqName) == null) {
+ logger.trace("air-Q - airqHandler - processType(): get({}) is null", airqName);
+ updateState(channelName, UnDefType.UNDEF);
+ if (type.contentEquals("pair")) {
+ updateState(channelName + "_maxerr", UnDefType.UNDEF);
+ }
+ } else {
+ switch (type) {
+ case "boolean":
+ String itemval = dec.get(airqName).toString();
+ if (itemval.contentEquals("true")) {
+ updateState(channelName, OnOffType.ON);
+ } else if (itemval.contentEquals("false")) {
+ updateState(channelName, OnOffType.OFF);
+ }
+ logger.trace("air-Q - airqHandler - processType(): channel {} set to {}", channelName, itemval);
+ break;
+ case "string":
+ case "time":
+ String strstr = dec.get(airqName).toString();
+ updateState(channelName, new StringType(strstr.substring(1, strstr.length() - 1)));
+ logger.trace("air-Q - airqHandler - processType(): channel {} set to {}", channelName, strstr);
+ break;
+ case "number":
+ updateState(channelName, new DecimalType(dec.get(airqName).toString()));
+ logger.trace("air-Q - airqHandler - processType(): channel {} set to {}", channelName,
+ dec.get(airqName).toString());
+ break;
+ case "pair":
+ ResultPair pair = new ResultPair(dec.get(airqName).toString());
+ updateState(channelName, new DecimalType(pair.getvalue()));
+ updateState(channelName + "_maxerr", new DecimalType(pair.getmaxdev()));
+ logger.trace("air-Q - airqHandler - processType(): channel {} set to {}, channel {} set to {}",
+ channelName, pair.getvalue(), channelName + "_maxerr", pair.getmaxdev());
+ break;
+ case "datetime":
+ Long timest = Long.valueOf(dec.get(airqName).toString());
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
+ String timestampString = sdf.format(new Date(timest));
+ updateState(channelName, DateTimeType.valueOf(timestampString));
+ logger.trace("air-Q - airqHandler - processType(): channel {} set to {} (original: {})",
+ channelName, timestampString, timest);
+ break;
+ case "coord":
+ JsonElement ans_coord = new Gson().fromJson(dec.get(airqName).toString(), JsonElement.class);
+ if (ans_coord != null) {
+ JsonObject json_coord = ans_coord.getAsJsonObject();
+ Float latitude = json_coord.get("lat").getAsFloat();
+ Float longitude = json_coord.get("long").getAsFloat();
+ updateState(channelName, new PointType(new DecimalType(latitude), new DecimalType(longitude)));
+ } else {
+ logger.error("Cannot extract coordinates from this data: {}", dec.get(airqName).toString());
+ }
+ break;
+ case "nightmode":
+ JsonElement daynightdata = new Gson().fromJson(dec.get(airqName).toString(), JsonElement.class);
+ if (daynightdata != null) {
+ JsonObject json_daynightdata = daynightdata.getAsJsonObject();
+ processType(json_daynightdata, "StartDay", "nightmode_StartDay", "string");
+ processType(json_daynightdata, "StartNight", "nightmode_StartNight", "string");
+ processType(json_daynightdata, "BrightnessDay", "nightmode_BrightnessDay", "number");
+ processType(json_daynightdata, "BrightnessNight", "nightmode_BrightnessNight", "number");
+ processType(json_daynightdata, "FanNightOff", "nightmode_FanNightOff", "boolean");
+ processType(json_daynightdata, "WifiNightOff", "nightmode_WifiNightOff", "boolean");
+ } else {
+ logger.error("Cannot extract day/night data: {}", dec.get(airqName).toString());
+ }
+ break;
+ case "wlan":
+ JsonElement wlandata = new Gson().fromJson(dec.get(airqName).toString(), JsonElement.class);
+ if (wlandata != null) {
+ JsonObject json_wlandata = wlandata.getAsJsonObject();
+ processType(json_wlandata, "Gateway", "WLAN_config_Gateway", "string");
+ processType(json_wlandata, "MAC", "WLAN_config_MAC", "string");
+ processType(json_wlandata, "SSID", "WLAN_config_SSID", "string");
+ processType(json_wlandata, "IP address", "WLAN_config_IPAddress", "string");
+ processType(json_wlandata, "Net Mask", "WLAN_config_NetMask", "string");
+ processType(json_wlandata, "BSSID", "WLAN_config_BSSID", "string");
+ } else {
+ logger.error("Cannot extract WLAN data from this string: {}", dec.get(airqName).toString());
+ }
+ break;
+ case "arr":
+ JsonElement jsonarr = new Gson().fromJson(dec.get(airqName).toString(), JsonElement.class);
+ if ((jsonarr != null) && (jsonarr.isJsonArray())) {
+ JsonArray arr = jsonarr.getAsJsonArray();
+ String str = new String();
+ for (JsonElement el : arr) {
+ str = str.concat(el.getAsString()).concat(", ");
+ }
+ logger.trace("air-Q - airqHandler - processType(): channel {} set to {}", channelName,
+ str.substring(0, str.length() - 2));
+ updateState(channelName, new StringType(str.substring(0, str.length() - 2)));
+ } else {
+ logger.error("air-Q - airqHandler - processType(): cannot handle this as an array: {}",
+ jsonarr);
+ }
+ break;
+ case "calib":
+ JsonElement lastcalib = new Gson().fromJson(dec.get(airqName).toString(), JsonElement.class);
+ if (lastcalib != null) {
+ JsonObject calibobj = lastcalib.getAsJsonObject();
+ String str = new String();
+ Long timecalib;
+ SimpleDateFormat sdfcalib = new SimpleDateFormat("dd.MM.yyyy' 'HH:mm:ss");
+ for (Entry entry : calibobj.entrySet()) {
+ String attributeName = entry.getKey();
+ JsonObject attributeValue = (JsonObject) entry.getValue();
+ timecalib = Long.valueOf(attributeValue.get("timestamp").toString());
+ String timecalibString = sdfcalib.format(new Date(timecalib * 1000));
+ str = str.concat(attributeName).concat(": offset=")
+ .concat(attributeValue.get("offset").getAsString()).concat(" [")
+ .concat(timecalibString).concat("]");
+ }
+ logger.trace("air-Q - airqHandler - processType(): channel {} set to {}", channelName,
+ str.substring(0, str.length() - 1));
+ updateState(channelName, new StringType(str.substring(0, str.length() - 1)));
+ } else {
+ logger.error("Cannot extract calibration data from this string: {}",
+ dec.get(airqName).toString());
+ }
+ break;
+ // JsonArray calibarr = lastcalib.getAsJsonArray();
+ // logger.trace("air-Q - airqHandler - processType(): calibarr={}, isarr={}", lastcalib,
+ // lastcalib.isJsonArray());
+ // for (JsonElement el : calibarr) {
+ // logger.trace("air-Q - airqHandler - processType(): lastcalib element {}", el);
+
+ // }
+ case "property":
+ String propstr = dec.get(airqName).toString();
+ getThing().setProperty(channelName, propstr);
+ logger.trace("air-Q - airqHandler - processType(): property {} set to {}", channelName, propstr);
+ break;
+ case "proparr":
+ JsonElement proparr = new Gson().fromJson(dec.get(airqName).toString(), JsonElement.class);
+ if ((proparr != null) && proparr.isJsonArray()) {
+ JsonArray arr = proparr.getAsJsonArray();
+ String arrstr = new String();
+ for (JsonElement el : arr) {
+ arrstr = arrstr.concat(el.getAsString()).concat(", ");
+ }
+ logger.trace("air-Q - airqHandler - processType(): property array {} set to {}", channelName,
+ arrstr.substring(0, arrstr.length() - 2));
+ getThing().setProperty(channelName, arrstr.substring(0, arrstr.length() - 2));
+ } else {
+ logger.error("air-Q - airqHandler - processType(): cannot handle this as an array: {}",
+ proparr);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private void changeSettings(JsonObject jsonchange) {
+ String jsoncmd = jsonchange.toString();
+ logger.trace("air-Q - airqHandler - changeSettings(): called with jsoncmd={}", jsoncmd);
+ if ((!ipaddress.equals("")) && (!password.equals(""))) {
+ Result res = null;
+ try {
+ String url = "http://".concat(ipaddress.concat("/config"));
+ String jsonbody = encrypt(jsoncmd.getBytes(), (String) (getThing().getConfiguration().get("password")));
+ String fullbody = "request=".concat(jsonbody);
+ // String testdecode = decrypt(jsonbody.getBytes(),
+ // (String) (getThing().getConfiguration().get("password")));
+ // logger.trace("air-Q - airqHandler - changeSettings(): testdecode={}, ", testdecode);
+ logger.trace("air-Q - airqHandler - changeSettings(): doing call to url={}, method=POST, body={}", url,
+ fullbody);
+ res = getData(url, "POST", fullbody);
+ if (res != null) {
+ Gson gson = new Gson();
+ JsonElement ans = gson.fromJson(res.getBody(), JsonElement.class);
+ if (ans != null) {
+ JsonObject jsonObj = ans.getAsJsonObject();
+ String jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(),
+ (String) (getThing().getConfiguration().get("password")));
+ logger.trace("air-Q - airqHandler - changeSettings(): call returned {}", jsonAnswer);
+ } else {
+ logger.error("The air-Q data could not be extracted from this string: {}", ans);
+ }
+ }
+ } catch (Exception e) {
+ System.out.println(
+ "air-Q - airqHandler - prepareChangeSettings(): Error while changing settings in air-Q data: "
+ + e.toString());
+ }
+ }
+ }
+
+ private void getDataFiles() {
+ Result res = null;
+ String url = "http://".concat(ipaddress.concat("/dirbuff"));
+ try {
+ File f_base = createDataDir("/air-q_data");
+ if (f_base.isDirectory()) {
+ res = getData(url, "GET", null);
+ if (res != null) {
+ /*
+ * res = doNetwork(url, "GET", null);
+ * if (res == null) {
+ * if (thStatus != ThingStatus.OFFLINE) {
+ * logger.error(
+ * "air-Q - airqHandler - getDataFiles(): cannot reach air-Q device. Status set to OFFLINE.");
+ * updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ * thStatus = ThingStatus.OFFLINE;
+ * } else {
+ * logger.warn(
+ * "air-Q - airqHandler - getDataFiles(): retried but still cannot reach the air-Q device.");
+ * }
+ * } else {
+ * if (thStatus == ThingStatus.OFFLINE) {
+ * logger.error(
+ * "air-Q - airqHandler - getDataFiles(): can reach air-Q device again, Status set back to ONLINE."
+ * );
+ * thStatus = ThingStatus.ONLINE;
+ * updateStatus(ThingStatus.ONLINE);
+ * }
+ */ logger.trace("air-Q - airqHandler - getDataFiles(): Result from doNetwork is {} with body={}",
+ res, res.getBody());
+ String answer = decrypt(res.getBody().getBytes(),
+ (String) (getThing().getConfiguration().get("password")));
+ logger.trace("air-Q - airqHandler - getDataFiles(): Result after decrypt: {}", answer);
+ // We got the directory and file structure. Now iterate through all files and copy them to the file
+ // system
+ Gson gson = new Gson();
+ JsonElement gsonEl = gson.fromJson(answer, JsonElement.class);
+ if (gsonEl != null) {
+ JsonObject jsonObj = gsonEl.getAsJsonObject();
+ Iterator ityr = jsonObj.keySet().iterator();
+ while (ityr.hasNext()) {
+ String year = ityr.next();
+ JsonObject jsonmonths = jsonObj.getAsJsonObject(year);
+ Iterator itmon = jsonmonths.keySet().iterator();
+ while (itmon.hasNext()) {
+ String month = itmon.next();
+ JsonObject jsondays = jsonmonths.getAsJsonObject(month);
+ Iterator itday = jsondays.keySet().iterator();
+ while (itday.hasNext()) {
+ String day = itday.next();
+ File f_day = createDataDir("/air-q_data/" + year + "/" + month + "/" + day);
+ if (f_day.isDirectory()) {
+ JsonArray jsonfilearr = jsondays.getAsJsonArray(day);
+ for (JsonElement el : jsonfilearr) {
+ String filename = el.getAsString();
+ String fullfilename = "air-q_data/" + year + "/" + month + "/" + day + "/"
+ + filename;
+ // We test if the file exists already. If it does, we do not download it
+ // again.
+ File f = new File(fullfilename);
+ if (f.isFile()) {
+ logger.trace("Element in year {}, month {}, day {}, file {}", year,
+ month, day, filename);
+ String encodedFileRequest = encrypt(
+ (year + "/" + month + "/" + day + "/" + filename).getBytes(),
+ (String) (getThing().getConfiguration().get("password")));
+ String fileurl = "http://".concat(
+ ipaddress.concat("/file?request=").concat(encodedFileRequest));
+ res = getData(fileurl, "GET", null);
+ if (res != null) {
+ FileWriter datafile = new FileWriter(fullfilename);
+ logger.debug("Writing data to {}", fullfilename);
+ for (String line : res.getBody().split("\\n")) {
+ String decodedText = decrypt(line.getBytes(),
+ (String) (getThing().getConfiguration()
+ .get("password")));
+ datafile.append(decodedText);
+ }
+ datafile.close();
+ }
+ } else {
+ logger.debug("Skipping file {} as it exists already", fullfilename);
+ }
+ }
+ }
+ }
+ }
+ }
+ } else {
+ logger.error("No data received; answer cannot be interpreted. Answer={}", answer);
+ }
+
+ }
+ }
+ } catch (
+
+ Exception e) {
+ System.out.println("Error in getDataFiles(): " + e.toString());
+ }
+ }
+
+ private File createDataDir(String dir) {
+ File f = new File(System.getProperty("user.dir") + dir);
+ if (f.exists()) {
+ if (!f.isDirectory()) {
+ logger.error(
+ "Cannot create or use directory {} as there is already a file (and not a directory) with that name",
+ dir);
+ }
+ } else {
+ if (!f.mkdir()) {
+ logger.error("Cannot create or use directory {} as the directory could not be created.", dir);
+ }
+ }
+ return f;
+ }
+
+ private File createDataFile(String dir) {
+ File f = new File(System.getProperty("user.dir") + dir);
+ if (f.exists()) {
+ if (!f.isFile()) {
+ logger.error(
+ "Cannot create or use file {} as there is already such an entry, but not a file (maybe a directory) with that name",
+ dir);
+ } else if (!f.canWrite()) {
+ logger.error("Cannot write file {}", dir);
+ }
+ } else {
+ try {
+ if (!f.createNewFile()) {
+ logger.error("Cannot create new file {}.", dir);
+ }
+ } catch (IOException exc) {
+ logger.error("Error while creating data file {}: ", dir, exc);
+ }
+ }
+ return f;
+ }
+};
diff --git a/bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/airqHandlerFactory.java b/bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/airqHandlerFactory.java
new file mode 100644
index 0000000000000..a767cc5bf3ec0
--- /dev/null
+++ b/bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/airqHandlerFactory.java
@@ -0,0 +1,81 @@
+/**
+ * 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.airq.internal;
+
+import static org.openhab.binding.airq.internal.airqBindingConstants.THING_TYPE_AIRQ;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link airqHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Aurelio Caliaro - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.airq", service = ThingHandlerFactory.class)
+public class airqHandlerFactory extends BaseThingHandlerFactory {
+
+ private final Logger logger = LoggerFactory.getLogger(airqHandlerFactory.class);
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_AIRQ);
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+ if (THING_TYPE_AIRQ.equals(thingTypeUID)) {
+ return new airqHandler(thing);
+ }
+ return null;
+ }
+
+ @Override
+ public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration,
+ @Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) {
+ logger.trace(
+ "air-Q - airqHandlerFactory - createThing: start with thingTypeUID={}, configuration={}, thingUID={}, bridgeUID={}",
+ thingTypeUID, configuration, thingUID, bridgeUID);
+ Thing th = super.createThing(thingTypeUID, configuration, thingUID, bridgeUID);
+ logger.trace("air-Q - airqHandlerFactory - createThing: result Thing={}", th);
+ return th;
+ /*
+ * if (airqBindingConstants.THING_TYPE_UID_BRIDGE.equals(thingTypeUID)) {
+ * logger.warn("Create Bridge: {}", adapterID);
+ * return super.createThing(thingTypeUID, configuration, thingUID, null);
+ * } else {
+ * if (supportsThingType(thingTypeUID)) {
+ * logger.trace("Create Thing: {}", adapterID);
+ * return super.createThing(thingTypeUID, configuration, thingUID, bridgeUID);
+ * }
+ */
+ }
+}
diff --git a/bundles/org.openhab.binding.airq/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.airq/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 0000000000000..a72e986a624f7
--- /dev/null
+++ b/bundles/org.openhab.binding.airq/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ air-Q Binding
+ This is the binding for air-Q devices.
+ Aurelio Caliaro
+
+
diff --git a/bundles/org.openhab.binding.airq/src/main/resources/OH-INF/i18n/airq_de_DE.properties b/bundles/org.openhab.binding.airq/src/main/resources/OH-INF/i18n/airq_de_DE.properties
new file mode 100644
index 0000000000000..43bd43fa764f4
--- /dev/null
+++ b/bundles/org.openhab.binding.airq/src/main/resources/OH-INF/i18n/airq_de_DE.properties
@@ -0,0 +1,125 @@
+# binding
+binding.airq.name = air-Q
+binding.airq.description = Binding für air-Q-Gerät
+
+# thing types
+thing-type.airq.airq.label = air-Q
+thing-type.airq.airq.description = Thing für air-Q-Gerät
+
+# thing type config description
+config.airq.airq.ipAddress.label = Netzwerk-Adresse
+config.airq.sample.config1.description = Netzwerk-Adresse, unter der das air-Q erreichbar ist
+
+# channel types
+channel-type.airq.devid.label = Gerätenummer aus Datenbezug
+channel-type.airq.devid.description = Interne Nummer des air-Q
+channel-type.airq.status.label = Sensorenstatus
+channel-type.airq.status.description = Status der internen Sensoren
+channel-type.airq.typps.label = Durchschn. Staubgrösse (Experimentell)
+channel-type.airq.typps.description = Durchschnittliche Grösse des Feinstaubs
+channel-type.airq.bat.label = Batteriestatus
+channel-type.airq.bat.description = Stand der Batterie, sofern vorhanden
+channel-type.airq.cnt0_3.label = Feinstaub >0,3 μm
+channel-type.airq.cnt0_3.description = Feinstaubpartikel grösser 0,3 μm
+channel-type.airq.cnt0_5.label = Feinstaub >0,5 μm
+channel-type.airq.cnt0_5.description = Feinstaubpartikel grösser 0,5 μm
+channel-type.airq.cnt1.label = Feinstaub >1,0 μm
+channel-type.airq.cnt1.description = Feinstaubpartikel grösser 1,0 μm
+channel-type.airq.cnt2_5.label = Feinstaub >2,5 μm
+channel-type.airq.cnt2_5.description = Feinstaubpartikel grösser 2,5 μm
+channel-type.airq.cnt5.label = Feinstaub >5 μm
+channel-type.airq.cnt5.description = Feinstaubpartikel grösser 5 μm
+channel-type.airq.cnt10.label = Feinstaub >10 μm
+channel-type.airq.cnt10.description = Feinstaubpartikel grösser 10 μm
+channel-type.airq.co2.label = CO2
+channel-type.airq.co2.description = CO2
+channel-type.airq.dco2dt.label = Änderung CO2-Wert
+channel-type.airq.dco2dt.description = Änderung CO2-Wert
+channel-type.airq.dhdt.label = Feuchtigkeitsänderung
+channel-type.airq.dhdt.description = Feuchtigkeitsänderung
+channel-type.airq.dewpt.label = Taupunkt
+channel-type.airq.dewpt.description = Taupunkt
+channel-type.airq.door.label = Tür (experimentell)
+channel-type.airq.door.description = Tür wurde geöffnet
+channel-type.airq.health.label = Gesundheitsindex
+channel-type.airq.health.description = Gesundheitsindex
+channel-type.airq.humidity.label = Feuchtigkeit
+channel-type.airq.humidity.description = Feuchtigkeit
+channel-type.airq.humidity_abs.label = Absolute Feuchtigkeit
+channel-type.airq.humidity_abs.description = Absolute Feuchtigkeit
+channel-type.airq.mtime.label = Messdauer
+channel-type.airq.mtime.description = Dauer eines Messzyklus
+channel-type.airq.no2.label = NO2-Konzentration
+channel-type.airq.no2.description = NO2-Konzentration
+channel-type.airq.o3.label = O3-Konzentration
+channel-type.airq.o3.description = O3-Konzentration
+channel-type.airq.oxygen.label = Sauerstoff-Konzentration
+channel-type.airq.oxygen.description = O2-Konzentration (Sauerstoff)
+channel-type.airq.performance.label = Leistung
+channel-type.airq.performance.description = Leistungsindex
+channel-type.airq.pm1.label = Feinstaubkonzentration >1μ
+channel-type.airq.pm1.description = Konzentration Feinstaub >1μ
+channel-type.airq.pm10.label = Feinstaubkonzentration >10μ
+channel-type.airq.pm10.description = Konzentration Feinstaub >10μ
+channel-type.airq.pm2_5.label = Feinstaubkonzentration >2,5μ
+channel-type.airq.pm2_5.description = Konzentration Feinstaub >2,5μ
+channel-type.airq.pressure.label = Luftdruck
+channel-type.airq.pressure.description = Luftdruck
+channel-type.airq.so2.label = SO2-Konzentration
+channel-type.airq.so2.description = SO2-Konzentration
+channel-type.airq.sound.label = Lautstärke
+channel-type.airq.sound.description = Lautstärke
+channel-type.airq.temperature.label = Temperatur
+channel-type.airq.temperature.description = Temperatur
+channel-type.airq.timestamp.label = Messzeitpunkt
+channel-type.airq.timestamp.description = Messzeitpunkt
+channel-type.airq.tvoc.label = VOC-Konzentration
+channel-type.airq.tvoc.description = Konzentration organischer Chemikalien
+channel-type.airq.uptime.label = Laufzeit air-Q
+channel-type.airq.uptime.description = Laufzeit air-Q
+
+channel-type.airq.bat_maxerr.label = Intervall Intervall Batteriestatus
+channel-type.airq.bat_maxerr.description = Intervall Stand der Batterie, sofern vorhanden
+channel-type.airq.cnt0_3_maxerr.label = Intervall Feinstaub >0,3 μm
+channel-type.airq.cnt0_3_maxerr.description = Intervall Feinstaubpartikel grösser 0,3 μm
+channel-type.airq.cnt0_5_maxerr.label = Intervall Feinstaub >0,5 μm
+channel-type.airq.cnt0_5_maxerr.description = Intervall Feinstaubpartikel grösser 0,5 μm
+channel-type.airq.cnt1_maxerr.label = Intervall Feinstaub >1,0 μm
+channel-type.airq.cnt1_maxerr.description = Intervall Feinstaubpartikel grösser 1,0 μm
+channel-type.airq.cnt2_5_maxerr.label = Intervall Feinstaub >2,5 μm
+channel-type.airq.cnt2_5_maxerr.description = Intervall Feinstaubpartikel grösser 2,5 μm
+channel-type.airq.cnt5_maxerr.label = Intervall Feinstaub >5 μm
+channel-type.airq.cnt5_maxerr.description = Intervall Feinstaubpartikel grösser 5 μm
+channel-type.airq.cnt10_maxerr.label = Intervall Feinstaub >10 μm
+channel-type.airq.cnt10_maxerr.description = Intervall Feinstaubpartikel grösser 10 μm
+channel-type.airq.co2_maxerr.label = Intervall CO2
+channel-type.airq.co2_maxerr.description = Intervall CO2
+channel-type.airq.dewpt_maxerr.label = Intervall Taupunkt
+channel-type.airq.dewpt_maxerr.description = Intervall Taupunkt
+channel-type.airq.humidity_maxerr.label = Intervall Feuchtigkeit
+channel-type.airq.humidity_maxerr.description = Intervall Feuchtigkeit
+channel-type.airq.humidity_abs_maxerr.label = Intervall Absolute Feuchtigkeit
+channel-type.airq.humidity_abs_maxerr.description = Intervall Absolute Feuchtigkeit
+channel-type.airq.no2_maxerr.label = Intervall NO2-Konzentration
+channel-type.airq.no2_maxerr.description = Intervall NO2-Konzentration
+channel-type.airq.o3_maxerr.label = Intervall O3-Konzentration
+channel-type.airq.o3_maxerr.description = Intervall O3-Konzentration
+channel-type.airq.oxygen_maxerr.label = Intervall Sauerstoff-Konzentration
+channel-type.airq.oxygen_maxerr.description = Intervall O2-Konzentration (Sauerstoff)
+channel-type.airq.pm1_maxerr.label = Intervall Feinstaubkonzentration >1μ
+channel-type.airq.pm1_maxerr.description = Intervall Konzentration Feinstaub >1μ
+channel-type.airq.pm10_maxerr.label = Intervall Feinstaubkonzentration >10μ
+channel-type.airq.pm10_maxerr.description = Intervall Konzentration Feinstaub >10μ
+channel-type.airq.pm2_5_maxerr.label = Intervall Feinstaubkonzentration >2,5μ
+channel-type.airq.pm2_5_maxerr.description = Intervall Konzentration Feinstaub >2,5μ
+channel-type.airq.pressure_maxerr.label = Intervall Luftdruck
+channel-type.airq.pressure_maxerr.description = Intervall Luftdruck
+channel-type.airq.so2_maxerr.label = Intervall SO2-Konzentration
+channel-type.airq.so2_maxerr.description = Intervall SO2-Konzentration
+channel-type.airq.sound_maxerr.label = Intervall Lautstärke
+channel-type.airq.sound_maxerr.description = Intervall Lautstärke
+channel-type.airq.temperature_maxerr.label = Intervall Temperatur
+channel-type.airq.temperature_maxerr.description = Intervall Temperatur
+channel-type.airq.tvoc_maxerr.label = Intervall VOC-Konzentration
+channel-type.airq.tvoc_maxerr.description = Intervall Konzentration organischer Chemikalien
+
diff --git a/bundles/org.openhab.binding.airq/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.airq/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..63a9e0609abd3
--- /dev/null
+++ b/bundles/org.openhab.binding.airq/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,685 @@
+
+
+
+
+
+
+ Thing for air-Q Device
+ Sensor
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Unknown Device ID
+ Unknown Hardware version
+ Unknown Software version
+ Unknown sensor list
+ No info about sensors
+ No industry info
+
+
+
+
+ network-address
+
+ The IP Network Address where the air-Q can be reached.
+
+
+ password
+
+ The Password of the air-Q device.
+
+
+
+
+
+
+
+
+ String
+
+
+
+
+
+ Number:Length
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number:Pressure
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ DateTime
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number
+
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+
+
+ Switch
+
+
+
+
+
+ String
+
+
+
+
+
+ String
+
+
+
+
+
+ Switch
+
+
+
+
+ String
+
+
+
+
+ Location
+
+
+
+
+ String
+
+
+
+
+ String
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+ Switch
+
+
+
+
+ Switch
+
+
+
+
+ String
+
+
+
+
+
+ String
+
+
+
+
+ String
+
+
+
+
+ String
+
+
+
+
+
+ Switch
+
+
+
+
+ String
+
+
+
+
+
+ String
+
+
+
+
+
+ String
+
+
+
+
+
+ String
+
+
+
+
+
+ String
+
+
+
+
+
+ String
+
+
+
+
+
+ Switch
+
+
+
+
+ Number
+
+
+
+
+ String
+
+
+
+
+ Switch
+
+
+
+
+ Switch
+
+
+
+
+ Switch
+
+
+
+
+ Switch
+
+
+
+
+ Switch
+
+
+
+
+ Switch
+
+
+
+
+ Switch
+
+
+
+
+ String
+
+
+
+
+
+ Switch
+
+
+
+
+
+ Switch
+
+
+
+
+ Switch
+
+
+
+
+ Switch
+
+
+
+
diff --git a/bundles/org.openhab.binding.airq/src/main/resources/configuration.png b/bundles/org.openhab.binding.airq/src/main/resources/configuration.png
new file mode 100644
index 0000000000000..58fe38b3de273
Binary files /dev/null and b/bundles/org.openhab.binding.airq/src/main/resources/configuration.png differ
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 79efb9c54e682..9efaaa3052bcb 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -40,6 +40,7 @@
org.openhab.transform.xslt
org.openhab.binding.adorne
+ org.openhab.binding.airq
org.openhab.binding.airquality
org.openhab.binding.airvisualnode
org.openhab.binding.alarmdecoder