]> git.basschouten.com Git - openhab-addons.git/commitdiff
[airq] Air-Q binding Initial contribution (#10048)
authoraurelio1 <aurelio@caliaro.net>
Thu, 29 Apr 2021 16:23:35 +0000 (18:23 +0200)
committerGitHub <noreply@github.com>
Thu, 29 Apr 2021 16:23:35 +0000 (18:23 +0200)
Signed-off-by: Aurelio Caliaro <aurelio@caliaro.net>
15 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.airq/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.airq/README.md [new file with mode: 0644]
bundles/org.openhab.binding.airq/doc/image_air-Q.png [new file with mode: 0644]
bundles/org.openhab.binding.airq/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.airq/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/AirqBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/AirqConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/AirqHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.airq/src/main/java/org/openhab/binding/airq/internal/AirqHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.airq/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.airq/src/main/resources/OH-INF/i18n/airq_de_DE.properties [new file with mode: 0644]
bundles/org.openhab.binding.airq/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index 403c77efe527e5c2e95a678ec0bdb5ab2b92711a..8ffad8e5199841cfcff6cad036c6ba8ae5ad6998 100644 (file)
@@ -9,6 +9,7 @@
 /bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers
 /bundles/org.openhab.automation.pidcontroller/ @fwolter
 /bundles/org.openhab.binding.adorne/ @theiding
+/bundles/org.openhab.binding.airq/ @aurelio1
 /bundles/org.openhab.binding.airquality/ @kubawolanin
 /bundles/org.openhab.binding.airvisualnode/ @3cky
 /bundles/org.openhab.binding.alarmdecoder/ @bobadair @billfor
index 0037c5b9d53250622f3f308f04064d0421143d48..385c85c2d4045644f48849f2aeb7764a8792d51e 100644 (file)
       <artifactId>org.openhab.binding.adorne</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.airq</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.airquality</artifactId>
diff --git a/bundles/org.openhab.binding.airq/NOTICE b/bundles/org.openhab.binding.airq/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.airq/README.md b/bundles/org.openhab.binding.airq/README.md
new file mode 100644 (file)
index 0000000..dd581d8
--- /dev/null
@@ -0,0 +1,223 @@
+# air-Q Binding
+
+The air-Q Binding integrates the air analyzer [air-Q](http://www.air-q.com) device into the openHAB system.
+
+With the binding, it is possible to subscribe to all data delivered by the air-Q device.
+
+![air-Q image](doc/image_air-Q.png)
+
+## Supported Things
+
+Only one Thing is supported: The `airq` device.
+This Binding was tested with an `air-Q Pro` device with 14 sensors. It also works with an `air-Q` device with 11 sensors.
+
+## Discovery
+
+Auto-discovery is not supported.
+
+## 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                     |
+| hardwareVersion        | Hardware version              |
+| softwareVersion        | Firmware version              |
+| sensorList             | 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.
+For the Maximum Error channels just add `_maxerr` to the channel names.
+
+The rw column is empty if the channel is only readable, w if the channel can be written and rw if it allows both to be read and written.
+
+| channel                   | type                 | rw | description                                                         |
+|---------------------------|----------------------|--------------------------------------------------------------------------|
+| status                    | String               |    | Status of the sensors (usually "OK")                                |
+| avgFineDustSize           | Number:Length        |    | Average size of Fine Dust [experimental]                            |
+| fineDustCnt00_3           | Number:Dimensionless |    | Fine Dust >0,3 µm                                                   |
+| fineDustCnt00_5           | Number:Dimensionless |    | Fine Dust >0,5 µm                                                   |
+| fineDustCnt01             | Number:Dimensionless |    | Fine Dust >1 µm                                                     |
+| fineDustCnt02_5           | Number:Dimensionless |    | Fine Dust >2,5 µm                                                   |
+| fineDustCnt05             | Number:Dimensionless |    | Fine Dust >5 µm                                                     |
+| fineDustCnt10             | Number:Dimensionless |    | Fine Dust >10 µm                                                    |
+| co                        | Number               |    | CO concentration                                                    |
+| co2                       | Number:Dimensionless |    | CO₂ concentration                                                   |
+| dCO2dt                    | Number               |    | Change of CO₂ concentration                                         |
+| dHdt                      | Number               |    | Change of Humidity                                                  |
+| dewpt                     | Number:Temperature   |    | Dew Point                                                           |
+| doorEvent                 | Number               |    | Door Event (experimental, might not work reliably)                  |
+| health                    | Number:Dimensionless |    | Health Index (0 to 1000, -200 for gas alarm, -800 for fire alarm)   |
+| humidityRelative          | Number:Dimensionless |    | Humidity in percent                                                 |
+| humidityAbsolute          | Number               |    | Absolute Humidity                                                   |
+| measureTime               | Number:Time          |    | Milliseconds needed for measurement                                 |
+| no2                       | Number               |    | NO₂ concentration                                                   |
+| o3                        | Number               |    | Ozone (O₃) concentration                                            |
+| o2                        | Number:Dimensionless |    | Oxygen (O₂) concentration                                           |
+| performance               | Number:Dimensionless |    | Performance Index (0 to 1000)                                       |
+| fineDustConc01            | Number               |    | Fine Dust concentration >1 µm                                       |
+| fineDustConc02_5          | Number               |    | Fine Dust concentration >2.5 µm                                     |
+| fineDustConc10            | Number               |    | Fine Dust concentration >10 µm             fni                      |
+| pressure                  | Number:Pressure      |    | Pressure                                                            |
+| so2                       | Number               |    | SO₂ concentration                                                   |
+| sound                     | Number:Dimensionless |    | Noise                                                               |
+| temperature               | Number:Temperature   |    | Temperature                                                         |
+| timestamp                 | DateTime             |    | Timestamp of measurement                                            |
+| tvoc                      | Number:Dimensionless |    | VOC concentration                                                   |
+| uptime                    | Number:Time          |    | uptime in seconds                                                   |
+| wifi                      | Switch               |    | WLAN on or off                                                      |
+| ssid                      | String               |    | WLAN SSID                                                           |
+| password                  | String               | w  | Device Password                                                     |
+| wifiInfo                  | Switch               | rw | Show WLAN status with LED                                           |
+| timeServer                | String               | rw | Name of Timeserver address                                          |
+| location                  | Location             | rw | Location of air-Q device                                            |
+| nightmodeStartDay         | String               | rw | Time to start day operation                                         |
+| nightmodeStartNight       | String               | rw | End of day operation                                                |
+| nightmodeBrightnessDay    | Number:Dimensionless | rw | Brightness of LED during the day                                    |
+| nightmodeBrightnessNight  | Number:Dimensionless | rw | Brightness of LED at night                                          |
+| nightmodeFanNightOff      | Switch               | rw | Switch off fan at night                                             |
+| nightmodeWifiNightOff     | Switch               | rw | Switch off WLAN at night                                            |
+| deviceName                | String               |    | Device Name                                                         |
+| roomType                  | String               | rw | Type of room                                                        |
+| logLevel                  | String               | w  | Logging level                                                       |
+| deleteKey                 | String               | w  | Settings to be deleted                                              |
+| fireAlarm                 | Switch               | rw | Send Fire Alarm if certain levels are met                           |
+| wlanConfigGateway         | String               | rw | Network Gateway                                                     |
+| wlanConfigMac             | String               | rw | MAC Address                                                         |
+| wlanConfigSsid            | String               | rw | WLAN SSID                                                           |
+| wlanConfigIPAddress       | String               | rw | Assigned IP address                                                 |
+| wlanConfigNetMask         | String               | rw | Network mask                                                        |
+| wlanConfigBssid           | String               | rw | Network BSSID                                                       |
+| cloudUpload               | Switch               | rw | Upload to air-Q cloud                                               |
+| averagingRhythm           | Number               | rw | Rhythm of measurement for historic average                          |
+| powerFreqSuppression      | String               | rw | Power Frequency                                                     |
+| autoDriftCompensation     | Switch               | rw | Compensate automatic drift                                          |
+| autoUpdate                | Switch               | rw | Install Firmware updates automatically                              |
+| advancedDataProcessing    | Switch               | rw | Use advanced algorithms eg. for open window or presence of a person |
+| ppm_and_ppb               | Switch               | rw | Output CO as ppm and NO₂, O₃ and SO₂ as ppb value instead of mg/m3  |
+| gasAlarm                  | Switch               | rw | Send Gas Alarm if certain levels are met                            |
+| soundPressure             | Switch               | rw | Sound Pressure Level                                                |
+| alarmForwarding           | Switch               | rw | 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               | rw | Do an average                                                       |
+| errorBars                 | Switch               | rw | Calculate Maximum Errors                                            |
+| warmupPhase               | Switch               | rw | Output data as Warmup Phase                                         |
+
+## Example
+
+### air-Q.things
+
+```
+Thing airq:airq:1 "air-Q" [ ipAddress="192.168.0.68", password="myAirQPassword" ]
+```
+
+### air-Q.items
+
+```
+String                airQ_status                 "Status of Sensors"                     {channel="airq:airq:1:status"}
+Number:Length         airQ_avgFineDustSize        "Average Size of Fine Dust"             {channel="airq:airq:1:avgFineDustSize"}
+Number:Dimensionless  airQ_fineDustCnt00_3        "Fine Dust >0,3 µm"                     {channel="airq:airq:1:fineDustCnt00_3"}
+Number:Dimensionless  airQ_fineDustCnt00_5        "Fine Dust >0,5 µm"                     {channel="airq:airq:1:fineDustCnt00_5"}
+Number:Dimensionless  airQ_fineDustCnt01          "Fine Dust >1,0 µm"                     {channel="airq:airq:1:fineDustCnt01"}
+Number:Dimensionless  airQ_fineDustCnt02_5        "Fine Dust >2,5 µm"                     {channel="airq:airq:1:fineDustCnt02_5"}
+Number:Dimensionless  airQ_fineDustCnt05          "Fine Dust >5 µm"                       {channel="airq:airq:1:fineDustCnt05"}
+Number:Dimensionless  airQ_fineDustCnt10          "Fine Dust >10 µm"                      {channel="airq:airq:1:fineDustCnt10"}
+Number                airQ_co                     "CO Concentration"                      {channel="airq:airq:1:co"}
+Number:Dimensionless  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"}
+Number                airQ_doorEvent              "Door Event (exp.)"                     {channel="airq:airq:1:doorEvent"}
+Number:Dimensionless  airQ_health                 "Health Index"                          {channel="airq:airq:1:health"}
+Number:Dimensionless  airQ_humidityRelative       "Humidity"                              {channel="airq:airq:1:humidityRelative"}
+Number                airQ_humidityAbsolute       "Absolute Humidity"                     {channel="airq:airq:1:humidityAbsolute"}
+Number:Time           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_o2                     "Oxygen concentration"                  {channel="airq:airq:1:o2"}
+Number:Dimensionless  airQ_performance            "Performance Index"                     {channel="airq:airq:1:performance"}
+Number                airQ_fineDustConc01         "Fine Dust Concentration >1µ"           {channel="airq:airq:1:fineDustConc01"}
+Number                airQ_fineDustConc02_5       "Fine Dust Concentration >2.5µ"         {channel="airq:airq:1:fineDustConc02_5"}
+Number                airQ_fineDustConc10         "Fine Dust Concentration >10µ"          {channel="airq:airq:1:fineDustConc10"}
+Number:Pressure       airQ_pressure               "Pressure"                              {channel="airq:airq:1:pressure"}
+Number                airQ_so2                    "SO2 concentration"                     {channel="airq:airq:1:so2"}
+Number:Dimensionless  airQ_sound                  "Noise"                                 {channel="airq:airq:1:sound"}
+Number:Temperature    airQ_temperature            "Temperature"                           {channel="airq:airq:1:temperature"}
+DateTime              airQ_timestamp              "TimeStamp [%1$td.%1$tm.%1$tY %1$tH:%1$tM]"                            {channel="airq:airq:1:timestamp"}
+Number:Dimensionless  airQ_voc                    "VOC concentration"                     {channel="airq:airq:1:tvoc"}
+Number:Time           airQ_uptime                 "Uptime"                                {channel="airq:airq:1:uptime"}
+
+Number:Dimensionless  airQ_cnt03_maxerr        "Maximum error of Fine Dust >0,3 µm"             {channel="airq:airq:1:cnt0_3_maxerr"}
+Number:Dimensionless  airQ_cnt05_maxerr        "Maximum error of Fine Dust >0,5 µm"             {channel="airq:airq:1:cnt0_5_maxerr"}
+Number:Dimensionless  airQ_cnt1_maxerr         "Maximum error of Fine Dust >1,0 µm"             {channel="airq:airq:1:cnt1_maxerr"}
+Number:Dimensionless  airQ_cnt25_maxerr        "Maximum error of Fine Dust >2,5 µm"             {channel="airq:airq:1:cnt2_5_maxerr"}
+Number:Dimensionless  airQ_cnt5_maxerr         "Maximum error of Fine Dust >5 µm"               {channel="airq:airq:1:cnt5_maxerr"}
+Number:Dimensionless  airQ_cnt10_maxerr        "Maximum error of Fine Dust >10 µm"              {channel="airq:airq:1:cnt10_maxerr"}
+Number:Dimensionless  airQ_co2_maxerr          "Maximum error of CO2 Concentration"             {channel="airq:airq:1:co2_maxerr"}
+Number:Dimensionless  airQ_dewpt_maxerr        "Maximum error of Dew Point"                     {channel="airq:airq:1:dewpt_maxerr"}
+Number:Dimensionless  airQ_humidity_maxerr     "Maximum error of Humidity"                      {channel="airq:airq:1:humidity_maxerr"}
+Number:Dimensionless  airQ_humidity_abs_maxerr "Maximum error of Absolute Humidity"             {channel="airq:airq:1:humidity_abs_maxerr"}
+Number:Dimensionless  airQ_no2_maxerr          "Maximum error of NO2 concentration"             {channel="airq:airq:1:no2_maxerr"}
+Number:Dimensionless  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:o2_maxerr"}
+Number:Dimensionless  airQ_pm1_maxerr          "Maximum error of Fine Dust Concentration >1µ"   {channel="airq:airq:1:pm1_maxerr"}
+Number:Dimensionless  airQ_pm2_5_maxerr        "Maximum error of Fine Dust Concentration >2.5µ" {channel="airq:airq:1:pm2_5_maxerr"}
+Number:Dimensionless  airQ_pm10_maxerr         "Maximum error of Fine Dust Concentration >10µ"  {channel="airq:airq:1:pm10_maxerr"}
+Number:Dimensionless  airQ_pressure_maxerr     "Maximum error of Pressure"                      {channel="airq:airq:1:pressure_maxerr"}
+Number:Dimensionless  airQ_so2_maxerr          "Maximum error of SO2 concentration"             {channel="airq:airq:1:so2_maxerr"}
+Number:Dimensionless  airQ_sound_maxerr        "Maximum error of Noise"                         {channel="airq:airq:1:sound_maxerr"}
+Number:Dimensionless  airQ_temperature_maxerr  "Maximum error of Temperature"                   {channel="airq:airq:1:temperature_maxerr"}
+Number:Dimensionless  airQ_voc_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_SSID                    "WLAN SSID"                                      {channel="airq:airq:1:ssid"}
+String airQ_password                "Device Password"                                {channel="airq:airq:1:password"}
+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_location              "Location of air-Q device"                       {channel="airq:airq:1:location"}
+String airQ_nightMode_startDay      "Time to start day operation"                    {channel="airq:airq:1:nightModeStartDay"}
+String airQ_nightMode_startNight    "End of day operation"                           {channel="airq:airq:1:nightModeStartNight"}
+Number:Dimensionless airQ_nightMode_brightnessDay "Brightness of LED during the day" {channel="airq:airq:1:nightModeBrightnessDay"}
+Number:Dimensionless airQ_nightMode_brightnessNight   "Brightness of LED at night"   {channel="airq:airq:1:nightModeBrightnessNight"}
+Switch airQ_nightMode_fanNightOff   "Switch off fan at night"                        {channel="airq:airq:1:nightModeFanNightOff"}
+Switch airQ_nightMode_wifiNightOff  "Switch off WLAN at night"                       {channel="airq:airq:1:nightModeWifiNightOff"}
+String airQ_deviceName              "Device Name"                                    {channel="airq:airq:1:deviceName"}
+String airQ_roomType                "Type of room"                                   {channel="airq:airq:1:roomType"}
+String airQ_logLevel                "Logging level"                                  {channel="airq:airq:1:logLevel"}
+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_WLAN_config_gateway     "Network Gateway"                                {channel="airq:airq:1:wlanConfigGateway"}
+String airQ_WLAN_config_MAC         "MAC Address"                                    {channel="airq:airq:1:wlanConfigMac"}
+String airQ_WLAN_config_SSID        "WLAN SSID"                                      {channel="airq:airq:1:wlanConfigSsid"}
+String airQ_WLAN_config_IPAddress   "Assigned IP address"                            {channel="airq:airq:1:wlanConfigIPAddress"}
+String airQ_WLAN_config_netMask     "Network mask"                                   {channel="airq:airq:1:wlanConfigNetMask"}
+String airQ_WLAN_config_BSSID       "Network BSSID"                                  {channel="airq:airq:1:wlanConfigBssid"}
+Switch airQ_cloudUpload             "Upload to air-Q cloud"                          {channel="airq:airq:1:cloudUpload"}
+Number airQ_averagingRhythm         "Rhythm of measurement for historic average"     {channel="airq:airq:1:averagingRhythm"}
+String airQ_powerFreqSuppression    "Power Frequency"                                {channel="airq:airq:1:powerFreqSuppression"}
+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_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"}
+Switch airQ_soundPressure           "Sound Pressure Level"                           {channel="airq:airq:1:soundPressure"}
+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"}
+Switch airQ_errorBars               "Calculate Maximum Errors"                       {channel="airq:airq:1:errorBars"}
+Switch airQ_warmupPhase             "Output Data as Warmup Phase"                    {channel="airq:airq:1:warmupPhase"}
+```
diff --git a/bundles/org.openhab.binding.airq/doc/image_air-Q.png b/bundles/org.openhab.binding.airq/doc/image_air-Q.png
new file mode 100644 (file)
index 0000000..fcc7240
Binary files /dev/null and b/bundles/org.openhab.binding.airq/doc/image_air-Q.png differ
diff --git a/bundles/org.openhab.binding.airq/pom.xml b/bundles/org.openhab.binding.airq/pom.xml
new file mode 100644 (file)
index 0000000..152fc6c
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  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.airq</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: air-Q Binding</name>
+
+</project>
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 (file)
index 0000000..8212fd3
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.airq-${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-airq" description="air-Q Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.airq/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..5ab53e1
--- /dev/null
@@ -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;
+
+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";
+    public static final ThingTypeUID THING_TYPE_AIRQ = new ThingTypeUID(BINDING_ID, "airq");
+}
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 (file)
index 0000000..a5c75a4
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * 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;
+
+/**
+ * The {@link AirqConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Aurelio Caliaro - Initial contribution
+ */
+
+@NonNullByDefault
+public class AirqConfiguration {
+    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 (file)
index 0000000..bda8cdc
--- /dev/null
@@ -0,0 +1,789 @@
+/**
+ * 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.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+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.Map.Entry;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+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.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpHeader;
+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.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+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.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 retrieving all information from the air-Q device
+ * and change properties and channels accordingly.
+ *
+ * @author Aurelio Caliaro - Initial contribution
+ */
+@NonNullByDefault
+public class AirqHandler extends BaseThingHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(AirqHandler.class);
+    private final Gson gson = new Gson();
+    private @Nullable ScheduledFuture<?> pollingJob;
+    private @Nullable ScheduledFuture<?> getConfigDataJob;
+    protected static final int POLLING_PERIOD_DATA_MSEC = 15000; // in milliseconds
+    protected static final int POLLING_PERIOD_CONFIG = 1; // in minutes
+    protected final HttpClient httpClient;
+    AirqConfiguration config = new AirqConfiguration();
+
+    final class ResultPair {
+        private final float value;
+        private final float maxdev;
+
+        public float getValue() {
+            return value;
+        }
+
+        public float getMaxdev() {
+            return maxdev;
+        }
+
+        /**
+         * Expects a string consisting of two values as sent by the air-Q device
+         * and returns a corresponding object
+         *
+         * @param input string formed as this: [1234,56,789,012] (including the brackets)
+         * @return ResultPair object with the two values
+         */
+        public ResultPair(String input) {
+            value = Float.parseFloat(input.substring(1, input.indexOf(',')));
+            maxdev = Float.parseFloat(input.substring(input.indexOf(',') + 1, input.length() - 1));
+        }
+    }
+
+    public AirqHandler(Thing thing, HttpClient httpClient) {
+        super(thing);
+        this.httpClient = httpClient;
+        logger.warn("air-Q - airqHandler - constructor: httpClient={}", httpClient);
+    }
+
+    private boolean isTimeFormat(String str) {
+        try {
+            LocalTime.parse(str);
+        } catch (DateTimeParseException e) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        if ((command instanceof OnOffType) || (command instanceof StringType)) {
+            JsonObject newobj = new JsonObject();
+            JsonObject subjson = new JsonObject();
+            switch (channelUID.getId()) {
+                case "wifi":
+                    // we do not allow to switch off Wifi because otherwise we can't connect to the air-Q device anymore
+                    break;
+                case "wifiInfo":
+                    newobj.addProperty("WifiInfo", command == OnOffType.ON);
+                    changeSettings(newobj);
+                    break;
+                case "fireAlarm":
+                    newobj.addProperty("FireAlarm", command == OnOffType.ON);
+                    changeSettings(newobj);
+                    break;
+                case "cloudUpload":
+                    newobj.addProperty("cloudUpload", command == OnOffType.ON);
+                    changeSettings(newobj);
+                    break;
+                case "autoDriftCompensation":
+                    newobj.addProperty("AutoDriftCompensation", command == OnOffType.ON);
+                    changeSettings(newobj);
+                    break;
+                case "autoUpdate":
+                    // note that this property is binary but uses 1 and 0 instead of true and false
+                    newobj.addProperty("AutoUpdate", command == OnOffType.ON ? 1 : 0);
+                    changeSettings(newobj);
+                    break;
+                case "advancedDataProcessing":
+                    newobj.addProperty("AdvancedDataProcessing", command == OnOffType.ON);
+                    changeSettings(newobj);
+                    break;
+                case "gasAlarm":
+                    newobj.addProperty("GasAlarm", command == OnOffType.ON);
+                    changeSettings(newobj);
+                    break;
+                case "soundPressure":
+                    newobj.addProperty("SoundInfo", command == OnOffType.ON);
+                    changeSettings(newobj);
+                    break;
+                case "alarmForwarding":
+                    newobj.addProperty("AlarmForwarding", command == OnOffType.ON);
+                    changeSettings(newobj);
+                    break;
+                case "averaging":
+                    newobj.addProperty("averaging", command == OnOffType.ON);
+                    changeSettings(newobj);
+                    break;
+                case "errorBars":
+                    newobj.addProperty("ErrorBars", command == OnOffType.ON);
+                    changeSettings(newobj);
+                    break;
+                case "ppm_and_ppb":
+                    newobj.addProperty("ppm&ppb", command == OnOffType.ON);
+                    changeSettings(newobj);
+                case "nightmodeFanNightOff":
+                    subjson.addProperty("FanNightOff", command == OnOffType.ON);
+                    newobj.add("NightMode", subjson);
+                    changeSettings(newobj);
+                    break;
+                case "nightmodeWifiNightOff":
+                    subjson.addProperty("WifiNightOff", command == OnOffType.ON);
+                    newobj.add("NightMode", subjson);
+                    changeSettings(newobj);
+                    break;
+                case "SSID":
+                    JsonElement wifidatael = 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.isEmpty()) {
+                            newobj.addProperty("WiFibssid", bssid);
+                        }
+                        newobj.addProperty("reset", wifidataobj.get("reset").getAsString());
+                        changeSettings(newobj);
+                    } else {
+                        logger.warn("Cannot extract wlan data from this string: {}", wifidatael);
+                    }
+                    break;
+                case "timeServer":
+                    newobj.addProperty(channelUID.getId(), command.toString());
+                    changeSettings(newobj);
+                    break;
+                case "nightmodeStartDay":
+                    if (isTimeFormat(command.toString())) {
+                        subjson.addProperty("StartDay", command.toString());
+                        newobj.add("NightMode", subjson);
+                        changeSettings(newobj);
+                    } else {
+                        logger.warn(
+                                "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 "nightmodeStartNight":
+                    if (isTimeFormat(command.toString())) {
+                        subjson.addProperty("StartNight", command.toString());
+                        newobj.add("NightMode", subjson);
+                        changeSettings(newobj);
+                    } else {
+                        logger.warn(
+                                "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 "location":
+                    PointType pt = (PointType) command;
+                    subjson.addProperty("lat", pt.getLatitude());
+                    subjson.addProperty("long", pt.getLongitude());
+                    newobj.add("geopos", subjson);
+                    changeSettings(newobj);
+                    break;
+                case "nightmodeBrightnessDay":
+                    try {
+                        subjson.addProperty("BrightnessDay", Float.parseFloat(command.toString()));
+                        newobj.add("NightMode", subjson);
+                        changeSettings(newobj);
+                    } catch (NumberFormatException exc) {
+                        logger.warn(
+                                "air-Q - airqHandler - handleCommand(): {} only accepts a float value, and {} is not.",
+                                channelUID.getId(), command.toString());
+                    }
+                    break;
+                case "nightmodeBrightnessNight":
+                    try {
+                        subjson.addProperty("BrightnessNight", Float.parseFloat(command.toString()));
+                        newobj.add("NightMode", subjson);
+                        changeSettings(newobj);
+                    } catch (NumberFormatException exc) {
+                        logger.warn(
+                                "air-Q - airqHandler - handleCommand(): {} only accepts a float value, and {} is not.",
+                                channelUID.getId(), command.toString());
+                    }
+                    break;
+                case "roomType":
+                    newobj.addProperty("RoomType", command.toString());
+                    changeSettings(newobj);
+                    break;
+                case "logLevel":
+                    String ll = command.toString();
+                    if (ll.equals("Error") || ll.equals("Warning") || ll.equals("Info")) {
+                        newobj.addProperty("logging", ll);
+                        changeSettings(newobj);
+                    } else {
+                        logger.warn(
+                                "air-Q - airqHandler - handleCommand(): {} should be set to {} but it isn't a correct setting for the power frequency suppression (only 50Hz or 60Hz)",
+                                channelUID.getId(), command.toString());
+                    }
+                    break;
+                case "averagingRhythm":
+                    try {
+                        newobj.addProperty("SecondsMeasurementDelay", Integer.parseUnsignedInt(command.toString()));
+                    } catch (NumberFormatException exc) {
+                        logger.warn(
+                                "air-Q - airqHandler - handleCommand(): {} only accepts an integer value, and {} is not.",
+                                channelUID.getId(), command.toString());
+                    }
+                    break;
+                case "powerFreqSuppression":
+                    String newFreq = command.toString();
+                    if (newFreq.equals("50Hz") || newFreq.equals("60Hz") || newFreq.equals("50Hz+60Hz")) {
+                        newobj.addProperty("Rejection", newFreq);
+                        changeSettings(newobj);
+                    } else {
+                        logger.warn(
+                                "air-Q - airqHandler - handleCommand(): {} should be set to {} but it isn't a correct setting for the power frequency suppression (only 50Hz or 60Hz)",
+                                channelUID.getId(), command.toString());
+                    }
+                    break;
+                default:
+                    logger.warn(
+                            "air-Q - airqHandler - handleCommand(): unknown command {} received (channelUID={}, value={})",
+                            command, channelUID, command);
+            }
+        }
+    }
+
+    @Override
+    public void initialize() {
+        config = getThing().getConfiguration().as(AirqConfiguration.class);
+        updateStatus(ThingStatus.UNKNOWN);
+        // We don't have to test if ipAddress and password have been set because we have defined them
+        // as being 'required' in thing-types.xml and OpenHAB will only initialize the handler if both are set.
+        String data = getDecryptedContentString("http://" + config.ipAddress + "/data", "GET", null);
+        // we try if the device is reachable and the password is correct. Otherwise a corresponding message is
+        // thrown in Thing manager.
+        if (data == null) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "Unable to retrieve get data from air-Q device. Probable cause: invalid password.");
+        } else {
+            updateStatus(ThingStatus.ONLINE);
+        }
+        pollingJob = scheduler.scheduleWithFixedDelay(this::pollData, 0, POLLING_PERIOD_DATA_MSEC,
+                TimeUnit.MILLISECONDS);
+        getConfigDataJob = scheduler.scheduleWithFixedDelay(this::getConfigData, 0, POLLING_PERIOD_CONFIG,
+                TimeUnit.MINUTES);
+    }
+
+    // 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(): content to decrypt: {}", 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');
+        }
+        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(Arrays.copyOf(encodedtextwithIV, 16));
+            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+            byte[] decryptedText = cipher.doFinal(ciphertext);
+            content = new String(decryptedText, StandardCharsets.UTF_8);
+            logger.trace("air-Q - airqHandler - decrypt(): Text decoded as String: {}", content);
+        } catch (BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException
+                | InvalidAlgorithmParameterException | IllegalBlockSizeException exc) {
+            logger.warn("Error while decrypting. Probably the provided password is wrong.");
+            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(StandardCharsets.UTF_8), 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);
+        } catch (Exception e) {
+            logger.warn("air-Q - airqHandler - encrypt(): Error while encrypting: {}", e.toString());
+        }
+        return content;
+    }
+
+    // gets the data after online/offline management and does the JSON work, or at least the first step.
+    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 getData() is {} with body={}",
+                    res, res.getBody());
+            // Gson code based on https://riptutorial.com/de/gson
+            JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
+            if (ans != null) {
+                JsonObject jsonObj = ans.getAsJsonObject();
+                jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
+                if (jsonAnswer == null) {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                            "Decryption not possible, probably wrong password");
+                }
+            } else {
+                logger.warn(
+                        "air-Q - airqHandler - getDecryptedContentString(): The air-Q data could not be extracted from this string: {}",
+                        ans);
+            }
+        }
+        return jsonAnswer;
+    }
+
+    // calls 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;
+        int timeout = 10;
+        logger.debug("air-Q - airqHandler - getData(): connecting to {} with method {} and body {}", address,
+                requestMethod, body);
+        Request request = httpClient.newRequest(address).timeout(timeout, TimeUnit.SECONDS).method(requestMethod);
+        if (body != null) {
+            request = request.content(new StringContentProvider(body)).header(HttpHeader.CONTENT_TYPE,
+                    "application/json");
+        }
+        try {
+            ContentResponse response = request.send();
+            res = new Result(response.getContentAsString(), response.getStatus());
+        } catch (InterruptedException | ExecutionException | TimeoutException exc) {
+            logger.warn("air-Q - airqHandler - doNetwork(): Error while accessing air-Q: {}", exc.toString());
+        }
+        if (res == null) {
+            if (getThing().getStatus() != ThingStatus.OFFLINE) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "air-Q device not reachable");
+            } else {
+                logger.warn("air-Q - airqHandler - getData(): retried but still cannot reach the air-Q device.");
+            }
+        } else {
+            if (getThing().getStatus() == ThingStatus.OFFLINE) {
+                updateStatus(ThingStatus.ONLINE);
+            }
+        }
+        return res;
+    }
+
+    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);
+        }
+    }
+
+    public void pollData() {
+        logger.trace("air-Q - airqHandler - run(): starting polled data handler");
+        try {
+            String url = "http://" + config.ipAddress + "/data";
+            String jsonAnswer = getDecryptedContentString(url, "GET", null);
+            if (jsonAnswer != null) {
+                JsonElement decEl = gson.fromJson(jsonAnswer, JsonElement.class);
+                if (decEl != null) {
+                    JsonObject decObj = decEl.getAsJsonObject();
+                    logger.debug("air-Q - airqHandler - run(): decObj={}, jsonAnswer={}", decObj, jsonAnswer);
+                    // 'bat' is a field that is already delivered by air-Q but as
+                    // there are no air-Q devices which are powered with batteries
+                    // it is obsolete at this moment. We implemented the code anyway
+                    // to make it easier to add afterwords, but for the moment it is not applicable.
+                    // processType(decObj, "bat", "battery", "pair");
+                    processType(decObj, "cnt0_3", "fineDustCnt00_3", "pair");
+                    processType(decObj, "cnt0_5", "fineDustCnt00_5", "pair");
+                    processType(decObj, "cnt1", "fineDustCnt01", "pair");
+                    processType(decObj, "cnt2_5", "fineDustCnt02_5", "pair");
+                    processType(decObj, "cnt5", "fineDustCnt05", "pair");
+                    processType(decObj, "cnt10", "fineDustCnt10", "pair");
+                    processType(decObj, "co", "co", "pair");
+                    processType(decObj, "co2", "co2", "pairPPM");
+                    processType(decObj, "dewpt", "dewpt", "pair");
+                    processType(decObj, "humidity", "humidityRelative", "pair");
+                    processType(decObj, "humidity_abs", "humidityAbsolute", "pair");
+                    processType(decObj, "no2", "no2", "pair");
+                    processType(decObj, "o3", "o3", "pair");
+                    processType(decObj, "oxygen", "o2", "pair");
+                    processType(decObj, "pm1", "fineDustConc01", "pair");
+                    processType(decObj, "pm2_5", "fineDustConc02_5", "pair");
+                    processType(decObj, "pm10", "fineDustConc10", "pair");
+                    processType(decObj, "pressure", "pressure", "pair");
+                    processType(decObj, "so2", "so2", "pair");
+                    processType(decObj, "sound", "sound", "pairDB");
+                    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
+                    // something like processType(decObj, "DeviceID", "DeviceID", "string") 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, "Status", "status", "string");
+                    processType(decObj, "TypPS", "avgFineDustSize", "number");
+                    processType(decObj, "dCO2dt", "dCO2dt", "number");
+                    processType(decObj, "dHdt", "dHdt", "number");
+                    processType(decObj, "door_event", "doorEvent", "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", "numberTimePeriod");
+                    processType(decObj, "tvoc", "tvoc", "pairPPB");
+                } else {
+                    logger.warn("The air-Q data could not be extracted from this string: {}", decEl);
+                }
+            }
+        } catch (Exception e) {
+            logger.warn("air-Q - airqHandler - polldata.run(): Error while retrieving air-Q data: {}", toString());
+        }
+    }
+
+    public void getConfigData() {
+        Result res = null;
+        logger.trace("air-Q - airqHandler - getConfigData(): starting processing data");
+        try {
+            String url = "http://" + config.ipAddress + "/config";
+            res = getData(url, "GET", null);
+            if (res != null) {
+                String jsontext = res.getBody();
+                logger.trace("air-Q - airqHandler - getConfigData(): Result from getBody() is {} with body={}", res,
+                        res.getBody());
+                JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
+                if (ans != null) {
+                    JsonObject jsonObj = ans.getAsJsonObject();
+                    String jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
+                    if (jsonAnswer != null) {
+                        JsonElement decEl = gson.fromJson(jsonAnswer, JsonElement.class);
+                        if (decEl != null) {
+                            JsonObject decObj = decEl.getAsJsonObject();
+                            logger.debug("air-Q - airqHandler - getConfigData(): decObj={}", decObj);
+                            processType(decObj, "Wifi", "wifi", "boolean");
+                            processType(decObj, "WLANssid", "ssid", "arr");
+                            processType(decObj, "pass", "password", "string");
+                            processType(decObj, "WifiInfo", "wifiInfo", "boolean");
+                            processType(decObj, "TimeServer", "timeServer", "string");
+                            processType(decObj, "geopos", "location", "coord");
+                            processType(decObj, "NightMode", "", "nightmode");
+                            processType(decObj, "devicename", "deviceName", "string");
+                            processType(decObj, "RoomType", "roomType", "string");
+                            processType(decObj, "logging", "logLevel", "string");
+                            processType(decObj, "DeleteKey", "deleteKey", "string");
+                            processType(decObj, "FireAlarm", "fireAlarm", "boolean");
+                            processType(decObj, "air-Q-Hardware-Version", "hardwareVersion", "property");
+                            processType(decObj, "WLAN config", "", "wlan");
+                            processType(decObj, "cloudUpload", "cloudUpload", "boolean");
+                            processType(decObj, "SecondsMeasurementDelay", "averagingRhythm", "number");
+                            processType(decObj, "Rejection", "powerFreqSuppression", "string");
+                            processType(decObj, "air-Q-Software-Version", "softwareVersion", "property");
+                            processType(decObj, "sensors", "sensorList", "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", "soundPressure", "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");
+                            processType(decObj, "warmup-phase", "warmupPhase", "boolean");
+                        } else {
+                            logger.warn(
+                                    "air-Q - airqHandler - getConfigData(): The air-Q data could not be extracted from this string: {}",
+                                    decEl);
+                        }
+                    }
+                } else {
+                    logger.warn(
+                            "air-Q - airqHandler - getConfigData(): The air-Q data could not be extracted from this string: {}",
+                            ans);
+                }
+            }
+        } catch (Exception e) {
+            logger.warn("air-Q - airqHandler - getConfigData(): 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={}", airqName, channelName,
+                type);
+        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") || itemval.contentEquals("1")) {
+                        updateState(channelName, OnOffType.ON);
+                    } else if (itemval.contentEquals("false") || itemval.contentEquals("0")) {
+                        updateState(channelName, OnOffType.OFF);
+                    }
+                    break;
+                case "string":
+                case "time":
+                    String strstr = dec.get(airqName).toString();
+                    updateState(channelName, new StringType(strstr.substring(1, strstr.length() - 1)));
+                    break;
+                case "number":
+                    updateState(channelName, new DecimalType(dec.get(airqName).toString()));
+                    break;
+                case "numberTimePeriod":
+                    updateState(channelName, new QuantityType<>(dec.get(airqName).getAsBigInteger(), Units.SECOND));
+                    break;
+                case "pair":
+                    ResultPair pair = new ResultPair(dec.get(airqName).toString());
+                    updateState(channelName, new DecimalType(pair.getValue()));
+                    updateState(channelName + "_maxerr", new DecimalType(pair.getMaxdev()));
+                    break;
+                case "pairPPM":
+                    ResultPair pairPPM = new ResultPair(dec.get(airqName).toString());
+                    updateState(channelName, new QuantityType<>(pairPPM.getValue(), Units.PARTS_PER_MILLION));
+                    updateState(channelName + "_maxerr", new DecimalType(pairPPM.getMaxdev()));
+                    break;
+                case "pairPPB":
+                    ResultPair pairPPB = new ResultPair(dec.get(airqName).toString());
+                    updateState(channelName, new QuantityType<>(pairPPB.getValue(), Units.PARTS_PER_BILLION));
+                    updateState(channelName + "_maxerr", new DecimalType(pairPPB.getMaxdev()));
+                    break;
+                case "pairDB":
+                    ResultPair pairDB = new ResultPair(dec.get(airqName).toString());
+                    logger.trace("air-Q - airqHandler - processType(): db transmitted as {} with unit {}",
+                            pairDB.getValue(), Units.DECIBEL);
+                    updateState(channelName, new QuantityType<>(pairDB.getValue(), Units.DECIBEL));
+                    updateState(channelName + "_maxerr", new DecimalType(pairDB.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));
+                    break;
+                case "coord":
+                    JsonElement ansCoord = gson.fromJson(dec.get(airqName).toString(), JsonElement.class);
+                    if (ansCoord != null) {
+                        JsonObject jsonCoord = ansCoord.getAsJsonObject();
+                        Float latitude = jsonCoord.get("lat").getAsFloat();
+                        Float longitude = jsonCoord.get("long").getAsFloat();
+                        updateState(channelName, new PointType(new DecimalType(latitude), new DecimalType(longitude)));
+                    } else {
+                        logger.warn(
+                                "air-Q - airqHandler - processType(): Cannot extract coordinates from this data: {}",
+                                dec.get(airqName).toString());
+                    }
+                    break;
+                case "nightmode":
+                    JsonElement daynightdata = gson.fromJson(dec.get(airqName).toString(), JsonElement.class);
+                    if (daynightdata != null) {
+                        JsonObject jsonDaynightdata = daynightdata.getAsJsonObject();
+                        processType(jsonDaynightdata, "StartDay", "nightModeStartDay", "string");
+                        processType(jsonDaynightdata, "StartNight", "nightModeStartNight", "string");
+                        processType(jsonDaynightdata, "BrightnessDay", "nightModeBrightnessDay", "number");
+                        processType(jsonDaynightdata, "BrightnessNight", "nightModeBrightnessNight", "number");
+                        processType(jsonDaynightdata, "FanNightOff", "nightModeFanNightOff", "boolean");
+                        processType(jsonDaynightdata, "WifiNightOff", "nightModeWifiNightOff", "boolean");
+                    } else {
+                        logger.warn("air-Q - airqHandler - processType(): Cannot extract day/night data: {}",
+                                dec.get(airqName).toString());
+                    }
+                    break;
+                case "wlan":
+                    JsonElement wlandata = gson.fromJson(dec.get(airqName).toString(), JsonElement.class);
+                    if (wlandata != null) {
+                        JsonObject jsonWlandata = wlandata.getAsJsonObject();
+                        processType(jsonWlandata, "Gateway", "wlanConfigGateway", "string");
+                        processType(jsonWlandata, "MAC", "wlanConfigMac", "string");
+                        processType(jsonWlandata, "SSID", "wlanConfigSsid", "string");
+                        processType(jsonWlandata, "IP address", "wlanConfigIPAddress", "string");
+                        processType(jsonWlandata, "Net Mask", "wlanConfigNetMask", "string");
+                        processType(jsonWlandata, "BSSID", "wlanConfigBssid", "string");
+                    } else {
+                        logger.warn(
+                                "air-Q - airqHandler - processType(): Cannot extract WLAN data from this string: {}",
+                                dec.get(airqName).toString());
+                    }
+                    break;
+                case "arr":
+                    JsonElement jsonarr = gson.fromJson(dec.get(airqName).toString(), JsonElement.class);
+                    if ((jsonarr != null) && (jsonarr.isJsonArray())) {
+                        JsonArray arr = jsonarr.getAsJsonArray();
+                        StringBuilder str = new StringBuilder();
+                        for (JsonElement el : arr) {
+                            str.append(el.getAsString() + ", ");
+                        }
+                        updateState(channelName, new StringType(str.substring(0, str.length() - 2)));
+                    } else {
+                        logger.warn("air-Q - airqHandler - processType(): cannot handle this as an array: {}", jsonarr);
+                    }
+                    break;
+                case "calib":
+                    JsonElement lastcalib = 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<String, JsonElement> 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 + attributeName + ": offset=" + attributeValue.get("offset").getAsString() + " ["
+                                    + timecalibString + "]";
+                        }
+                        updateState(channelName, new StringType(str.substring(0, str.length() - 1)));
+                    } else {
+                        logger.warn(
+                                "air-Q - airqHandler - processType(): Cannot extract calibration data from this string: {}",
+                                dec.get(airqName).toString());
+                    }
+                    break;
+                case "property":
+                    String propstr = dec.get(airqName).toString();
+                    getThing().setProperty(channelName, propstr);
+                    break;
+                case "proparr":
+                    JsonElement proparr = 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 + el.getAsString() + ", ";
+                        }
+                        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.warn("air-Q - airqHandler - processType(): cannot handle this as an array: {}", proparr);
+                    }
+                    break;
+                default:
+                    logger.warn(
+                            "air-Q - airqHandler - processType(): a setting of type {} should be changed but I don't know this type.",
+                            type);
+                    break;
+            }
+        }
+    }
+
+    private void changeSettings(JsonObject jsonchange) {
+        String jsoncmd = jsonchange.toString();
+        logger.trace("air-Q - airqHandler - changeSettings(): called with jsoncmd={}", jsoncmd);
+        Result res = null;
+        String url = "http://" + config.ipAddress + "/config";
+        String jsonbody = encrypt(jsoncmd.getBytes(StandardCharsets.UTF_8), config.password);
+        String fullbody = "request=" + jsonbody;
+        logger.trace("air-Q - airqHandler - changeSettings(): doing call to url={}, method=POST, body={}", url,
+                fullbody);
+        res = getData(url, "POST", fullbody);
+        if (res != null) {
+            JsonElement ans = gson.fromJson(res.getBody(), JsonElement.class);
+            if (ans != null) {
+                JsonObject jsonObj = ans.getAsJsonObject();
+                String jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
+                logger.trace("air-Q - airqHandler - changeSettings(): call returned {}", jsonAnswer);
+            } else {
+                logger.warn("The air-Q data could not be extracted from this string: {}", ans);
+            }
+        }
+    }
+};
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 (file)
index 0000000..7c8bc2a
--- /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.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.io.net.http.HttpClientFactory;
+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.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link AirqHandlerFactory} is responsible for creating the air-Q thing and its handlers.
+ *
+ * @author Aurelio Caliaro - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.airq", service = ThingHandlerFactory.class)
+public class AirqHandlerFactory extends BaseThingHandlerFactory {
+
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_AIRQ);
+    private final HttpClientFactory httpClientFactory;
+
+    @Activate
+    public AirqHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
+        this.httpClientFactory = httpClientFactory;
+    }
+
+    @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, httpClientFactory.getCommonHttpClient());
+        }
+        return null;
+    }
+}
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 (file)
index 0000000..f490e07
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="airq" 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>air-Q Binding</name>
+       <description>This is the binding for air-Q devices. The air-Q device contains several sensors measuring gases in the
+               air and other ambiance parameters like noise or temperature. With this binding you can integrate those values into
+               your openHAB system and change parametrs of the air-Q device.</description>
+
+</binding:binding>
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 (file)
index 0000000..3c274e2
--- /dev/null
@@ -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 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 (file)
index 0000000..47d5b2b
--- /dev/null
@@ -0,0 +1,666 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="airq"
+       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">
+
+       <thing-type id="airq">
+               <label>air-Q</label>
+               <description>Thing for air-Q Device</description>
+               <category>Sensor</category>
+
+               <channels>
+                       <channel id="status" typeId="status"/>
+                       <channel id="avgFineDustSize" typeId="typps"/>
+                       <channel id="fineDustCnt00_3" typeId="cnt0_3"/>
+                       <channel id="fineDustCnt00_5" typeId="cnt0_5"/>
+                       <channel id="fineDustCnt01" typeId="cnt1"/>
+                       <channel id="fineDustCnt02_5" typeId="cnt2_5"/>
+                       <channel id="fineDustCnt05" typeId="cnt5"/>
+                       <channel id="fineDustCnt10" typeId="cnt10"/>
+                       <channel id="co" typeId="co"/>
+                       <channel id="co2" typeId="co2"/>
+                       <channel id="dCO2dt" typeId="dco2dt"/>
+                       <channel id="dHdt" typeId="dhdt"/>
+                       <channel id="dewpt" typeId="dewpt"/>
+                       <channel id="doorEvent" typeId="door"/>
+                       <channel id="health" typeId="health"/>
+                       <channel id="humidityRelative" typeId="humidity"/>
+                       <channel id="humidityAbsolute" typeId="humidity_abs"/>
+                       <channel id="measureTime" typeId="mtime"/>
+                       <channel id="no2" typeId="no2"/>
+                       <channel id="o3" typeId="o3"/>
+                       <channel id="o2" typeId="oxygen"/>
+                       <channel id="performance" typeId="performance"/>
+                       <channel id="fineDustConc01" typeId="pm1"/>
+                       <channel id="fineDustConc02_5" typeId="pm2_5"/>
+                       <channel id="fineDustConc10" typeId="pm10"/>
+                       <channel id="pressure" typeId="pressure"/>
+                       <channel id="so2" typeId="so2"/>
+                       <channel id="sound" typeId="sound"/>
+                       <channel id="temperature" typeId="temperature"/>
+                       <channel id="timestamp" typeId="timestamp"/>
+                       <channel id="tvoc" typeId="tvoc"/>
+                       <channel id="uptime" typeId="uptime"/>
+
+                       <!-- Maximum error -->
+                       <channel id="fineDustCnt00_3_maxerr" typeId="cnt0_3_maxerr"/>
+                       <channel id="fineDustCnt00_5_maxerr" typeId="cnt0_5_maxerr"/>
+                       <channel id="fineDustCnt01_maxerr" typeId="cnt1_maxerr"/>
+                       <channel id="fineDustCnt02_5_maxerr" typeId="cnt2_5_maxerr"/>
+                       <channel id="fineDustCnt05_maxerr" typeId="cnt5_maxerr"/>
+                       <channel id="fineDustCnt10_maxerr" typeId="cnt10_maxerr"/>
+                       <channel id="co_maxerr" typeId="co_maxerr"/>
+                       <channel id="co2_maxerr" typeId="co2_maxerr"/>
+                       <channel id="dewpt_maxerr" typeId="dewpt_maxerr"/>
+                       <channel id="humidityRelative_maxerr" typeId="humidity_maxerr"/>
+                       <channel id="humidityAbsolute_maxerr" typeId="humidity_abs_maxerr"/>
+                       <channel id="no2_maxerr" typeId="no2_maxerr"/>
+                       <channel id="o3_maxerr" typeId="o3_maxerr"/>
+                       <channel id="o2_maxerr" typeId="oxygen_maxerr"/>
+                       <channel id="fineDustConc01_maxerr" typeId="pm1_maxerr"/>
+                       <channel id="fineDustConc02_5_maxerr" typeId="pm2_5_maxerr"/>
+                       <channel id="fineDustConc10_maxerr" typeId="pm10_maxerr"/>
+                       <channel id="pressure_maxerr" typeId="pressure_maxerr"/>
+                       <channel id="so2_maxerr" typeId="so2_maxerr"/>
+                       <channel id="sound_maxerr" typeId="sound_maxerr"/>
+                       <channel id="temperature_maxerr" typeId="temperature_maxerr"/>
+                       <channel id="tvoc_maxerr" typeId="tvoc_maxerr"/>
+
+                       <!-- configuration data -->
+                       <channel id="wifi" typeId="Wifi"/>
+                       <channel id="ssid" typeId="WLANssid"/>
+                       <channel id="password" typeId="pass"/>
+                       <channel id="wifiInfo" typeId="WifiInfo"/>
+                       <channel id="timeServer" typeId="TimeServer"/>
+                       <channel id="location" typeId="geopos"/>
+                       <channel id="nightModeStartDay" typeId="nightmodeStartDay"/>
+                       <channel id="nightModeStartNight" typeId="nightmodeStartNight"/>
+                       <channel id="nightModeBrightnessDay" typeId="nightmodeBrightnessDay"/>
+                       <channel id="nightModeBrightnessNight" typeId="nightmodeBrightnessNight"/>
+                       <channel id="nightModeFanNightOff" typeId="nightmodeFanNightOff"/>
+                       <channel id="nightModeWifiNightOff" typeId="nightmodeWifiNightOff"/>
+                       <channel id="deviceName" typeId="deviceName"/>
+                       <channel id="roomType" typeId="RoomType"/>
+                       <channel id="logLevel" typeId="Logging"/>
+                       <channel id="deleteKey" typeId="DeleteKey"/>
+                       <channel id="fireAlarm" typeId="FireAlarm"/>
+                       <channel id="wlanConfigGateway" typeId="WLAN_config_Gateway"/>
+                       <channel id="wlanConfigMac" typeId="WLAN_config_MAC"/>
+                       <channel id="wlanConfigSsid" typeId="WLAN_config_SSID"/>
+                       <channel id="wlanConfigIPAddress" typeId="WLAN_config_IPAddress"/>
+                       <channel id="wlanConfigNetMask" typeId="WLAN_config_NetMask"/>
+                       <channel id="wlanConfigBssid" typeId="WLAN_config_BSSID"/>
+                       <channel id="cloudUpload" typeId="cloudUpload"/>
+                       <channel id="averagingRhythm" typeId="SecondsMeasurementDelay"/>
+                       <channel id="powerFreqSuppression" typeId="Rejection"/>
+                       <channel id="autoDriftCompensation" typeId="AutoDriftCompensation"/>
+                       <channel id="autoUpdate" typeId="AutoUpdate"/>
+                       <channel id="advancedDataProcessing" typeId="AdvancedDataProcessing"/>
+                       <channel id="ppm_and_ppb" typeId="ppm_and_ppb"/>
+                       <channel id="gasAlarm" typeId="GasAlarm"/>
+                       <channel id="soundPressure" typeId="SoundInfo"/>
+                       <channel id="alarmForwarding" typeId="AlarmForwarding"/>
+                       <channel id="userCalib" typeId="usercalib"/>
+                       <channel id="initialCalFinished" typeId="InitialCalFinished"/>
+                       <channel id="averaging" typeId="Averaging"/>
+                       <channel id="errorBars" typeId="ErrorBars"/>
+                       <channel id="warmupPhase" typeId="WarmupPhase"/>
+               </channels>
+
+               <properties>
+                       <property name="id">Unknown Device ID</property>
+                       <property name="hardwareVersion">Unknown Hardware version</property>
+                       <property name="softwareVersion">Unknown Software version</property>
+                       <property name="sensorList">Unknown sensor list</property>
+                       <property name="sensorInfo">No info about sensors</property>
+                       <property name="industry">No industry info</property>
+               </properties>
+
+               <config-description>
+                       <parameter name="ipAddress" type="text" required="true">
+                               <context>network-address</context>
+                               <label>Network Address</label>
+                               <description>IP Network Address where air-Q Can Be Reached.</description>
+                       </parameter>
+                       <parameter name="password" type="text" required="true">
+                               <context>password</context>
+                               <label>Password</label>
+                               <description>Password of air-Q Device.</description>
+                       </parameter>
+               </config-description>
+
+       </thing-type>
+
+       <channel-type id="status" advanced="false">
+               <item-type>String</item-type>
+               <label>Status of Sensors</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="typps" advanced="false">
+               <item-type>Number:Length</item-type>
+               <label>Average Size of Fine Dust</label>
+               <state readOnly="true" pattern="%.2f μm"></state>
+       </channel-type>
+
+       <channel-type id="cnt0_3" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Fine Dust >0,3 μm</label>
+               <state readOnly="true" pattern="%.0f"></state>
+       </channel-type>
+
+       <channel-type id="cnt0_5" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Fine Dust >0,5 μm</label>
+               <state readOnly="true" pattern="%.0f"></state>
+       </channel-type>
+
+       <channel-type id="cnt1" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Fine Dust >1 μm</label>
+               <state readOnly="true" pattern="%.0f"></state>
+       </channel-type>
+
+       <channel-type id="cnt2_5" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Fine Dust >2,5 μm</label>
+               <state readOnly="true" pattern="%.0f"></state>
+       </channel-type>
+
+       <channel-type id="cnt5" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Fine Dust >5 μm</label>
+               <state readOnly="true" pattern="%.0f"></state>
+       </channel-type>
+
+       <channel-type id="cnt10" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Fine Dust >10 μm</label>
+               <state readOnly="true" pattern="%.0f"></state>
+       </channel-type>
+
+       <channel-type id="co" advanced="false">
+               <item-type>Number</item-type>
+               <label>CO Concentration</label>
+               <state readOnly="true" pattern="%.0f mg/m³"></state>
+       </channel-type>
+
+       <channel-type id="co2" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>CO₂ Concentration</label>
+               <state readOnly="true" pattern="%.0f %unit%"></state>
+       </channel-type>
+
+       <channel-type id="dco2dt" advanced="false">
+               <item-type>Number</item-type>
+               <label>Change of CO₂ Concentr.</label>
+               <state readOnly="true" pattern="%.2f ppm/s"></state>
+       </channel-type>
+
+       <channel-type id="dhdt" advanced="false">
+               <item-type>Number</item-type>
+               <label>Change of Humidity</label>
+               <state readOnly="true" pattern="%.2f g/m³/s"></state>
+       </channel-type>
+
+       <channel-type id="dewpt" advanced="false">
+               <item-type>Number:Temperature</item-type>
+               <label>Dew Point</label>
+               <state readOnly="true" pattern="%.3f %unit%"></state>
+       </channel-type>
+
+       <channel-type id="door" advanced="false">
+               <item-type>Number</item-type>
+               <label>Door Event (exp)</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="health" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Health Index</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="humidity" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Humidity</label>
+               <state readOnly="true" pattern="%.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="humidity_abs" advanced="false">
+               <item-type>Number</item-type>
+               <label>Absolute Humidity</label>
+               <state readOnly="true" pattern="%.3f g/m³"></state>
+       </channel-type>
+
+       <channel-type id="mtime" advanced="true">
+               <item-type>Number:Time</item-type>
+               <label>Time Needed for Measurement</label>
+               <state readOnly="true" pattern="%d ms"></state>
+       </channel-type>
+
+       <channel-type id="no2" advanced="false">
+               <item-type>Number</item-type>
+               <label>NO₂ Concentration</label>
+               <state readOnly="true" pattern="%.2f μg/m³"></state>
+       </channel-type>
+
+       <channel-type id="o3" advanced="false">
+               <item-type>Number</item-type>
+               <label>O₃ Concentration</label>
+               <state readOnly="true" pattern="%.2f μg/m³"></state>
+       </channel-type>
+
+       <channel-type id="oxygen" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Oxygen Concentration</label>
+               <state readOnly="true" pattern="%.3f %%"></state>
+       </channel-type>
+
+       <channel-type id="performance" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Performance Index</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="pm1" advanced="false">
+               <item-type>Number</item-type>
+               <label>Fine Dust Concentr. >1μ</label>
+               <state readOnly="true" pattern="%.0f μg/m³"></state>
+       </channel-type>
+
+       <channel-type id="pm10" advanced="false">
+               <item-type>Number</item-type>
+               <label>Fine Dust Concentr. >10μ</label>
+               <state readOnly="true" pattern="%.0f μg/m³"></state>
+       </channel-type>
+
+       <channel-type id="pm2_5" advanced="false">
+               <item-type>Number</item-type>
+               <label>Fine Dust Concentr. >2,5μ</label>
+               <state readOnly="true" pattern="%.0f μg/m³"></state>
+       </channel-type>
+
+       <channel-type id="pressure" advanced="false">
+               <item-type>Number:Pressure</item-type>
+               <label>Pressure</label>
+               <state readOnly="true" pattern="%.2f hPa"></state>
+       </channel-type>
+
+       <channel-type id="so2" advanced="false">
+               <item-type>Number</item-type>
+               <label>SO₂ Concentration</label>
+               <state readOnly="true" pattern="%.2f μg/m³"></state>
+       </channel-type>
+
+       <channel-type id="sound" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Noise</label>
+               <state readOnly="true" pattern="%.1f %unit%"></state>
+       </channel-type>
+
+       <channel-type id="temperature" advanced="false">
+               <item-type>Number:Temperature</item-type>
+               <label>Temperature</label>
+               <state readOnly="true" pattern="%.2f %unit%"></state>
+       </channel-type>
+
+       <channel-type id="timestamp" advanced="false">
+               <item-type>DateTime</item-type>
+               <label>Time Stamp</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="tvoc" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>VOC Concentration</label>
+               <state readOnly="true" pattern="%.0f %unit%"></state>
+       </channel-type>
+
+       <channel-type id="uptime" advanced="true">
+               <item-type>Number:Time</item-type>
+               <label>Uptime</label>
+               <state readOnly="true" pattern="%d %unit%"></state>
+       </channel-type>
+
+       <!-- Maximum error -->
+       <channel-type id="cnt0_3_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Fine Dust >0,3μm</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="cnt0_5_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Fine Dust >0,5μm</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="cnt1_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Fine Dust >1μm</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="cnt2_5_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Fine Dust >2,5μm</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="cnt5_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Fine Dust >5μm</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="cnt10_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Fine Dust >10μm</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="co_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error CO Conc.</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="co2_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error CO₂ Conc.</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="dewpt_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Dew Point</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="humidity_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Humidity</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="humidity_abs_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Abs. Humidity</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="no2_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error NO₂ Conc.</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="o3_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error O₃ Conc.</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="oxygen_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Oxygen Conc.</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="pm1_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Fine Dust Conc. >1μm</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="pm10_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Fine Dust Conc. >10μm</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="pm2_5_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Fine Dust Conc. >2,5μm</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="pressure_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Pressure</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="so2_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error SO₂ Conc.</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="sound_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Noise</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="temperature_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error Temperature</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <channel-type id="tvoc_maxerr" advanced="true">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Max. Error VOC Conc.</label>
+               <state readOnly="true" pattern="± %.2f %%"></state>
+       </channel-type>
+
+       <!-- settings -->
+
+       <channel-type id="Wifi" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Use WLAN</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="WLANssid" advanced="true">
+               <item-type>String</item-type>
+               <label>WLAN SSID</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="pass" advanced="true">
+               <item-type>String</item-type>
+               <label>Device Password</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="WifiInfo" advanced="false">
+               <item-type>Switch</item-type>
+               <label>Show WLAN Status with LED</label>
+       </channel-type>
+
+       <channel-type id="TimeServer" advanced="true">
+               <item-type>String</item-type>
+               <label>Time Server</label>
+       </channel-type>
+
+       <channel-type id="geopos" advanced="false">
+               <item-type>Location</item-type>
+               <label>Location of air-Q Device</label>
+       </channel-type>
+
+       <channel-type id="nightmodeStartDay" advanced="false">
+               <item-type>String</item-type>
+               <label>Start of Day Operation</label>
+       </channel-type>
+
+       <channel-type id="nightmodeStartNight" advanced="false">
+               <item-type>String</item-type>
+               <label>End of Day Operation</label>
+       </channel-type>
+
+       <channel-type id="nightmodeBrightnessDay" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Day Brightness of LED</label>
+       </channel-type>
+
+       <channel-type id="nightmodeBrightnessNight" advanced="false">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Night Brightness of LED</label>
+       </channel-type>
+
+       <channel-type id="nightmodeFanNightOff" advanced="false">
+               <item-type>Switch</item-type>
+               <label>Switch Off Fan at Night</label>
+       </channel-type>
+
+       <channel-type id="nightmodeWifiNightOff" advanced="false">
+               <item-type>Switch</item-type>
+               <label>Switch Off WLAN at Night</label>
+       </channel-type>
+
+       <channel-type id="deviceName" advanced="false">
+               <item-type>String</item-type>
+               <label>Device Name</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="RoomType" advanced="false">
+               <item-type>String</item-type>
+               <label>Room Type</label>
+       </channel-type>
+
+       <channel-type id="Logging" advanced="true">
+               <item-type>String</item-type>
+               <label>Logging Level</label>
+       </channel-type>
+
+       <channel-type id="DeleteKey" advanced="true">
+               <item-type>String</item-type>
+               <label>Settings to Be Deleted</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="FireAlarm" advanced="false">
+               <item-type>Switch</item-type>
+               <label>Fire Alarm</label>
+       </channel-type>
+
+       <channel-type id="WLAN_config_Gateway" advanced="true">
+               <item-type>String</item-type>
+               <label>Network Gateway</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="WLAN_config_MAC" advanced="true">
+               <item-type>String</item-type>
+               <label>MAC Address</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="WLAN_config_SSID" advanced="true">
+               <item-type>String</item-type>
+               <label>WLAN SSID</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="WLAN_config_IPAddress" advanced="true">
+               <item-type>String</item-type>
+               <label>Assigned IP Address</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="WLAN_config_NetMask" advanced="true">
+               <item-type>String</item-type>
+               <label>Network Mask</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="WLAN_config_BSSID" advanced="true">
+               <item-type>String</item-type>
+               <label>Network BSSID</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="cloudUpload" advanced="false">
+               <item-type>Switch</item-type>
+               <label>Upload to air-Q Cloud</label>
+       </channel-type>
+
+       <channel-type id="SecondsMeasurementDelay" advanced="true">
+               <item-type>Number</item-type>
+               <label>Rhythm Historic Average</label>
+               <state pattern="%d s"></state>
+       </channel-type>
+
+       <channel-type id="Rejection" advanced="true">
+               <item-type>String</item-type>
+               <label>Power Frequency</label>
+       </channel-type>
+
+       <channel-type id="AutoDriftCompensation" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Compensate Automatic Drift</label>
+       </channel-type>
+
+       <channel-type id="AutoUpdate" advanced="false">
+               <item-type>Switch</item-type>
+               <label>Automatic Firmware Update</label>
+       </channel-type>
+
+       <channel-type id="AdvancedDataProcessing" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Advanced Data Processing</label>
+       </channel-type>
+
+       <channel-type id="ppm_and_ppb" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Values in Particles</label>
+       </channel-type>
+
+       <channel-type id="GasAlarm" advanced="false">
+               <item-type>Switch</item-type>
+               <label>Gas Alarm</label>
+       </channel-type>
+
+       <channel-type id="SoundInfo" advanced="false">
+               <item-type>Switch</item-type>
+               <label>Sound Info</label>
+       </channel-type>
+
+       <channel-type id="AlarmForwarding" advanced="false">
+               <item-type>Switch</item-type>
+               <label>Share Alarms With Other air-Q</label>
+       </channel-type>
+
+       <channel-type id="usercalib" advanced="true">
+               <item-type>String</item-type>
+               <label>Last Sensor Calibration</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="InitialCalFinished" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Initial Calibration Done</label>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="Averaging" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Do Average</label>
+       </channel-type>
+
+       <channel-type id="ErrorBars" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Calculate Maximum Errors</label>
+       </channel-type>
+
+       <channel-type id="WarmupPhase" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Send data as in Warmup Phase</label>
+       </channel-type>
+
+</thing:thing-descriptions>
index 0e0b3f6e779473572f0d5250ab4ca9cb13c76521..bf2fe504d7d53e308a70114c24a223d8c0499411 100644 (file)
@@ -41,6 +41,7 @@
     <module>org.openhab.transform.xslt</module>
     <!-- bindings -->
     <module>org.openhab.binding.adorne</module>
+    <module>org.openhab.binding.airq</module>
     <module>org.openhab.binding.airquality</module>
     <module>org.openhab.binding.airvisualnode</module>
     <module>org.openhab.binding.alarmdecoder</module>