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