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 0000000000000..58fe38b3de273 Binary files /dev/null and b/bundles/org.openhab.binding.airq/src/main/resources/configuration.png differ diff --git a/bundles/pom.xml b/bundles/pom.xml index 79efb9c54e682..9efaaa3052bcb 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -40,6 +40,7 @@ org.openhab.transform.xslt org.openhab.binding.adorne + org.openhab.binding.airq org.openhab.binding.airquality org.openhab.binding.airvisualnode org.openhab.binding.alarmdecoder