[airq] Air-Q binding Initial contribution (#10048)

Signed-off-by: Aurelio Caliaro <aurelio@caliaro.net>
This commit is contained in:
aurelio1 2021-04-29 18:23:35 +02:00 committed by GitHub
parent 541a71d1a1
commit 315964a55a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1977 additions and 0 deletions

View 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

View File

@ -36,6 +36,11 @@
<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>

View File

@ -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

View File

@ -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"}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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");
}

View File

@ -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 = "";
}

View File

@ -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);
}
}
}
};

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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

View File

@ -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>

View 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>