added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.airquality</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,202 @@
# Air Quality Binding
This binding uses the [AQIcn.org service](https://aqicn.org) for providing air quality information for any location worldwide.
The World Air Quality Index project is a social enterprise project started in 2007.
Its mission is to promote Air Pollution awareness and provide a unified Air Quality information for the whole world.
The project is proving a transparent Air Quality information for more than 70 countries, covering more than 9000 stations in 600 major cities, via those two websites: [aqicn.org](https://aqicn.org) and [waqi.info](https://waqi.info).
To use this binding, you first need to [register and get your API token](https://aqicn.org/data-platform/token/).
## Supported Things
There is exactly one supported thing type, which represents the air quality information for an observation location.
It has the `aqi` id.
Of course, you can add multiple Things, e.g. for measuring AQI for different locations.
## Discovery
Local Air Quality can be autodiscovered based on system location.
You will have complete default configuration with your apiKey.
## Binding Configuration
The binding has no configuration options, all configuration is done at Thing level.
## Thing Configuration
The thing has a few configuration parameters:
| Parameter | Description |
|-----------|-------------------------------------------------------------------------|
| apikey | Data-platform token to access the AQIcn.org service. Mandatory. |
| location | Geo coordinates to be considered by the service. |
| stationId | Unique ID of the measuring station. |
| refresh | Refresh interval in minutes. Optional, the default value is 60 minutes. |
For the location parameter, the following syntax is allowed (comma separated latitude and longitude):
```java
37.8,-122.4
37.8255,-122.456
```
If you always want to receive data from specific station and you know its unique ID, you can enter it instead of the coordinates.
This `stationId` can be found by using the following link:
https://api.waqi.info/search/?token=TOKEN&keyword=NAME, replacing TOKEN by your apikey and NAME by the station you are looking for.
## Channels
The AirQuality information that is retrieved is available as these channels:
| Channel ID | Item Type | Description |
|-----------------|----------------------|----------------------------------------------|
| aqiLevel | Number | Air Quality Index |
| aqiColor | Color | Color associated to given AQI Index. |
| aqiDescription | String | AQI Description |
| locationName | String | Nearest measuring station location |
| stationId | Number | Measuring station ID |
| stationLocation | Location | Latitude/longitude of measuring station |
| pm25 | Number | Fine particles pollution level (PM2.5) |
| pm10 | Number | Coarse dust particles pollution level (PM10) |
| o3 | Number | Ozone level (O3) |
| no2 | Number | Nitrogen Dioxide level (NO2) |
| co | Number | Carbon monoxide level (CO) |
| so2 | Number | Sulfur dioxide level (SO2) |
| observationTime | DateTime | Observation date and time |
| temperature | Number:Temperature | Temperature in Celsius degrees |
| pressure | Number:Pressure | Pressure level |
| humidity | Number:Dimensionless | Humidity level |
| dominentpol | String | Dominent Polutor |
`AQI Description` item provides a human-readable output that can be interpreted e.g. by MAP transformation.
*Note that channels like* `pm25`, `pm10`, `o3`, `no2`, `co`, `so2` *can sometimes return* `UNDEF` *value due to the fact that some stations don't provide measurements for them.*
## Full Example
airquality.map:
```text
-=-
UNDEF=No data
NULL=No data
NO_DATA=No data
GOOD=Good
MODERATE=Moderate
UNHEALTHY_FOR_SENSITIVE=Unhealthy for sensitive groups
UNHEALTHY=Unhealthy
VERY_UNHEALTHY=Very unhealthy
HAZARDOUS=Hazardous
```
airquality.things:
```java
airquality:aqi:home "AirQuality" @ "Krakow" [ apikey="XXXXXXXXXXXX", location="50.06465,19.94498", refresh=60 ]
airquality:aqi:warsaw "AirQuality in Warsaw" [ apikey="XXXXXXXXXXXX", location="52.22,21.01", refresh=60 ]
airquality:aqi:brisbane "AirQuality in Brisbane" [ apikey="XXXXXXXXXXXX", stationId=5115 ]
```
airquality.items:
```java
Group AirQuality <flow>
Number Aqi_Level "Air Quality Index" <flow> (AirQuality) { channel="airquality:aqi:home:aqiLevel" }
String Aqi_Description "AQI Level [MAP(airquality.map):%s]" <flow> (AirQuality) { channel="airquality:aqi:home:aqiDescription" }
Number Aqi_Pm25 "PM\u2082\u2085 Level" <line> (AirQuality) { channel="airquality:aqi:home:pm25" }
Number Aqi_Pm10 "PM\u2081\u2080 Level" <line> (AirQuality) { channel="airquality:aqi:home:pm10" }
Number Aqi_O3 "O\u2083 Level" <line> (AirQuality) { channel="airquality:aqi:home:o3" }
Number Aqi_No2 "NO\u2082 Level" <line> (AirQuality) { channel="airquality:aqi:home:no2" }
Number Aqi_Co "CO Level" <line> (AirQuality) { channel="airquality:aqi:home:co" }
Number Aqi_So2 "SO\u2082 Level" <line> (AirQuality) { channel="airquality:aqi:home:so2" }
String Aqi_LocationName "Measuring Location" <settings> (AirQuality) { channel="airquality:aqi:home:locationName" }
Location Aqi_StationGeo "Station Location" <office> (AirQuality) { channel="airquality:aqi:home:stationLocation" }
Number Aqi_StationId "Station ID" <pie> (AirQuality) { channel="airquality:aqi:home:stationId" }
DateTime Aqi_ObservationTime "Time of observation [%1$tH:%1$tM]" <clock> (AirQuality) { channel="airquality:aqi:home:observationTime" }
Number:Temperature Aqi_Temperature "Temperature" <temperature> (AirQuality) { channel="airquality:aqi:home:temperature" }
Number:Pressure Aqi_Pressure "Pressure" <pressure> (AirQuality) { channel="airquality:aqi:home:pressure" }
Number:DimensionLess Aqi_Humidity "Humidity" <humidity> (AirQuality) { channel="airquality:aqi:home:humidity" }
```
airquality.sitemap:
```perl
sitemap airquality label="Air Quality" {
Frame {
Text item=Aqi_Level valuecolor=[
Aqi_Level=="-"="lightgray",
Aqi_Level>=300="#7e0023",
>=201="#660099",
>=151="#cc0033",
>=101="#ff9933",
>=51="#ffde33",
>=0="#009966"
]
Text item=Aqi_Description valuecolor=[
Aqi_Description=="HAZARDOUS"="#7e0023",
=="VERY_UNHEALTHY"="#660099",
=="UNHEALTHY"="#cc0033",
=="UNHEALTHY_FOR_SENSITIVE"="#ff9933",
=="MODERATE"="#ffde33",
=="GOOD"="#009966"
]
}
Frame {
Text item=Aqi_Pm25
Text item=Aqi_Pm10
Text item=Aqi_O3
Text item=Aqi_No2
Text item=Aqi_Co
Text item=Aqi_So2
}
Frame {
Text item=Aqi_LocationName
Text item=Aqi_ObservationTime
Text item=Aqi_Temperature
Text item=Aqi_Pressure
Text item=Aqi_Humidity
}
Frame label="Station Location" {
Mapview item=Aqi_StationGeo height=10
}
}
```
airquality.rules:
```java
rule "Change lamp color to reflect Air Quality"
when
Item Aqi_Description changed
then
var String hsb
switch Aqi_Description.state {
case "HAZARDOUS":
hsb = "343,100,49"
case "VERY_UNHEALTHY":
hsb = "280,100,60"
case "UNHEALTHY":
hsb = "345,100,80"
case "UNHEALTHY_FOR_SENSITIVE":
hsb = "30,80,100"
case "MODERATE":
hsb = "50,80,100"
case "GOOD":
hsb = "160,100,60"
}
Lamp_Color.sendCommand(hsb)
end
```

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.airquality</artifactId>
<name>openHAB Add-ons :: Bundles :: Airquality Binding</name>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.airquality-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-airquality" description="Air Quality Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.airquality/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2020 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.airquality.internal;
import static org.openhab.core.library.unit.MetricPrefix.HECTO;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.measure.Unit;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.Pressure;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.State;
/**
* The {@link AirQualityBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Kuba Wolanin - Initial contribution
* @author Łukasz Dywicki - Initial contribution
*/
@NonNullByDefault
public class AirQualityBindingConstants {
public static final String BINDING_ID = "airquality";
public static final String LOCAL = "local";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_AQI = new ThingTypeUID(BINDING_ID, "aqi");
// List of all Channel id's
public static final String AQI = "aqiLevel";
public static final String AQI_COLOR = "aqiColor";
public static final String AQIDESCRIPTION = "aqiDescription";
public static final String PM25 = "pm25";
public static final String PM10 = "pm10";
public static final String O3 = "o3";
public static final String NO2 = "no2";
public static final String CO = "co";
public static final String SO2 = "so2";
public static final String LOCATIONNAME = "locationName";
public static final String STATIONLOCATION = "stationLocation";
public static final String STATIONID = "stationId";
public static final String OBSERVATIONTIME = "observationTime";
public static final String TEMPERATURE = "temperature";
public static final String PRESSURE = "pressure";
public static final String HUMIDITY = "humidity";
public static final String DOMINENTPOL = "dominentpol";
public static final State GOOD = new StringType("GOOD");
public static final State MODERATE = new StringType("MODERATE");
public static final State UNHEALTHY_FOR_SENSITIVE = new StringType("UNHEALTHY_FOR_SENSITIVE");
public static final State UNHEALTHY = new StringType("UNHEALTHY");
public static final State VERY_UNHEALTHY = new StringType("VERY_UNHEALTHY");
public static final State HAZARDOUS = new StringType("HAZARDOUS");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_AQI);
public static final Set<String> SUPPORTED_CHANNEL_IDS = Stream.of(AQI, AQIDESCRIPTION, PM25, PM10, O3, NO2, CO, SO2,
LOCATIONNAME, STATIONLOCATION, STATIONID, OBSERVATIONTIME, TEMPERATURE, PRESSURE, HUMIDITY)
.collect(Collectors.toSet());
// Units of measurement of the data delivered by the API
public static final Unit<Temperature> API_TEMPERATURE_UNIT = SIUnits.CELSIUS;
public static final Unit<Dimensionless> API_HUMIDITY_UNIT = SmartHomeUnits.PERCENT;
public static final Unit<Pressure> API_PRESSURE_UNIT = HECTO(SIUnits.PASCAL);
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 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.airquality.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link AirQualityConfiguration} is the class used to match the
* thing configuration.
*
* @author Kuba Wolanin - Initial contribution
*/
@NonNullByDefault
public class AirQualityConfiguration {
public static final String LOCATION = "location";
public String apikey = "";
public String location = "";
public @Nullable Integer stationId;
public int refresh = 60;
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 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.airquality.internal;
import static org.openhab.binding.airquality.internal.AirQualityBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.airquality.internal.handler.AirQualityHandler;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import com.google.gson.Gson;
/**
* The {@link AirQualityHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Kuba Wolanin - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.airquality")
@NonNullByDefault
public class AirQualityHandlerFactory extends BaseThingHandlerFactory {
private final Gson gson = new Gson();
private final TimeZoneProvider timeZoneProvider;
@Activate
public AirQualityHandlerFactory(final @Reference TimeZoneProvider timeZoneProvider) {
this.timeZoneProvider = timeZoneProvider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_AQI.equals(thingTypeUID)) {
return new AirQualityHandler(thing, gson, timeZoneProvider);
}
return null;
}
}

View File

@@ -0,0 +1,122 @@
/**
* Copyright (c) 2010-2020 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.airquality.internal.discovery;
import static org.openhab.binding.airquality.internal.AirQualityBindingConstants.*;
import static org.openhab.binding.airquality.internal.AirQualityConfiguration.LOCATION;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.library.types.PointType;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AirQualityDiscoveryService} creates things based on the configured location.
*
* @author Gaël L'hopital - Initial Contribution
*/
@Component(service = DiscoveryService.class, configurationPid = "discovery.airquality")
@NonNullByDefault
public class AirQualityDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(AirQualityDiscoveryService.class);
private static final int DISCOVER_TIMEOUT_SECONDS = 10;
private static final int LOCATION_CHANGED_CHECK_INTERVAL = 60;
private final LocationProvider locationProvider;
private @Nullable ScheduledFuture<?> discoveryJob;
private @Nullable PointType previousLocation;
/**
* Creates a AirQualityDiscoveryService with enabled autostart.
*/
@Activate
public AirQualityDiscoveryService(@Reference LocationProvider locationProvider) {
super(SUPPORTED_THING_TYPES, DISCOVER_TIMEOUT_SECONDS, true);
this.locationProvider = locationProvider;
}
@Override
protected void activate(@Nullable Map<String, @Nullable Object> configProperties) {
super.activate(configProperties);
}
@Override
@Modified
protected void modified(@Nullable Map<String, @Nullable Object> configProperties) {
super.modified(configProperties);
}
@Override
protected void startScan() {
logger.debug("Starting Air Quality discovery scan");
PointType location = locationProvider.getLocation();
if (location == null) {
logger.debug("LocationProvider.getLocation() is not set -> Will not provide any discovery results");
return;
}
createResults(location);
}
@Override
protected void startBackgroundDiscovery() {
if (discoveryJob == null) {
discoveryJob = scheduler.scheduleWithFixedDelay(() -> {
PointType currentLocation = locationProvider.getLocation();
if (currentLocation != null && !Objects.equals(currentLocation, previousLocation)) {
logger.debug("Location has been changed from {} to {}: Creating new discovery results",
previousLocation, currentLocation);
createResults(currentLocation);
previousLocation = currentLocation;
}
}, 0, LOCATION_CHANGED_CHECK_INTERVAL, TimeUnit.SECONDS);
logger.debug("Scheduled Air Qualitylocation-changed job every {} seconds", LOCATION_CHANGED_CHECK_INTERVAL);
}
}
public void createResults(PointType location) {
ThingUID localAirQualityThing = new ThingUID(THING_TYPE_AQI, LOCAL);
Map<String, Object> properties = new HashMap<>();
properties.put(LOCATION, String.format("%s,%s", location.getLatitude(), location.getLongitude()));
thingDiscovered(DiscoveryResultBuilder.create(localAirQualityThing).withLabel("Local Air Quality")
.withProperties(properties).build());
}
@Override
protected void stopBackgroundDiscovery() {
logger.debug("Stopping Air Quality background discovery");
ScheduledFuture<?> job = this.discoveryJob;
if (job != null && !job.isCancelled()) {
if (job.cancel(true)) {
discoveryJob = null;
logger.debug("Stopped Air Quality background discovery");
}
}
}
}

View File

@@ -0,0 +1,285 @@
/**
* Copyright (c) 2010-2020 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.airquality.internal.handler;
import static org.openhab.binding.airquality.internal.AirQualityBindingConstants.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.airquality.internal.AirQualityConfiguration;
import org.openhab.binding.airquality.internal.json.AirQualityJsonData;
import org.openhab.binding.airquality.internal.json.AirQualityJsonResponse;
import org.openhab.binding.airquality.internal.json.AirQualityJsonResponse.ResponseStatus;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.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.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* The {@link AirQualityHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Kuba Wolanin - Initial contribution
* @author Łukasz Dywicki - Initial contribution
*/
@NonNullByDefault
public class AirQualityHandler extends BaseThingHandler {
private static final String URL = "http://api.waqi.info/feed/%QUERY%/?token=%apikey%";
private static final int REQUEST_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30);
private final Logger logger = LoggerFactory.getLogger(AirQualityHandler.class);
private @Nullable ScheduledFuture<?> refreshJob;
private final Gson gson;
private int retryCounter = 0;
private final TimeZoneProvider timeZoneProvider;
public AirQualityHandler(Thing thing, Gson gson, TimeZoneProvider timeZoneProvider) {
super(thing);
this.gson = gson;
this.timeZoneProvider = timeZoneProvider;
}
@Override
public void initialize() {
logger.debug("Initializing Air Quality handler.");
AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class);
logger.debug("config apikey = (omitted from logging)");
logger.debug("config location = {}", config.location);
logger.debug("config stationId = {}", config.stationId);
logger.debug("config refresh = {}", config.refresh);
List<String> errorMsg = new ArrayList<>();
if (config.apikey.trim().isEmpty()) {
errorMsg.add("Parameter 'apikey' is mandatory and must be configured");
}
if (config.location.trim().isEmpty() && config.stationId == null) {
errorMsg.add("Parameter 'location' or 'stationId' is mandatory and must be configured");
}
if (config.refresh < 30) {
errorMsg.add("Parameter 'refresh' must be at least 30 minutes");
}
if (errorMsg.isEmpty()) {
ScheduledFuture<?> job = this.refreshJob;
if (job == null || job.isCancelled()) {
refreshJob = scheduler.scheduleWithFixedDelay(this::updateAndPublishData, 0, config.refresh,
TimeUnit.MINUTES);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.join(", ", errorMsg));
}
}
private void updateAndPublishData() {
retryCounter = 0;
AirQualityJsonData aqiResponse = getAirQualityData();
if (aqiResponse != null) {
// Update all channels from the updated AQI data
getThing().getChannels().stream().filter(channel -> isLinked(channel.getUID().getId())).forEach(channel -> {
String channelId = channel.getUID().getId();
State state = getValue(channelId, aqiResponse);
updateState(channelId, state);
});
}
}
@Override
public void dispose() {
logger.debug("Disposing the Air Quality handler.");
ScheduledFuture<?> job = this.refreshJob;
if (job != null && !job.isCancelled()) {
job.cancel(true);
refreshJob = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
updateAndPublishData();
} else {
logger.debug("The Air Quality binding is read-only and can not handle command {}", command);
}
}
/**
* Build request URL from configuration data
*
* @return a valid URL for the aqicn.org service
*/
private String buildRequestURL() {
AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class);
String location = config.location.trim();
Integer stationId = config.stationId;
String geoStr = "geo:" + location.replace(" ", "").replace(",", ";").replace("\"", "").replace("'", "").trim();
String urlStr = URL.replace("%apikey%", config.apikey.trim());
return urlStr.replace("%QUERY%", stationId == null ? geoStr : "@" + stationId);
}
/**
* Request new air quality data to the aqicn.org service
*
* @param location geo-coordinates from config
* @param stationId station ID from config
* @return the air quality data object mapping the JSON response or null in case of error
*/
private @Nullable AirQualityJsonData getAirQualityData() {
String errorMsg;
String urlStr = buildRequestURL();
logger.debug("URL = {}", urlStr);
try {
String response = HttpUtil.executeUrl("GET", urlStr, null, null, null, REQUEST_TIMEOUT_MS);
logger.debug("aqiResponse = {}", response);
AirQualityJsonResponse result = gson.fromJson(response, AirQualityJsonResponse.class);
if (result.getStatus() == ResponseStatus.OK) {
AirQualityJsonData data = result.getData();
String attributions = data.getAttributions();
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, attributions);
return data;
} else {
retryCounter++;
if (retryCounter == 1) {
logger.warn("Error in aqicn.org, retrying once");
return getAirQualityData();
}
errorMsg = "Missing data sub-object";
logger.warn("Error in aqicn.org response: {}", errorMsg);
}
} catch (IOException e) {
errorMsg = e.getMessage();
} catch (JsonSyntaxException e) {
errorMsg = "Configuration is incorrect";
logger.warn("Error running aqicn.org request: {}", errorMsg);
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, errorMsg);
return null;
}
public State getValue(String channelId, AirQualityJsonData aqiResponse) {
String[] fields = channelId.split("#");
switch (fields[0]) {
case AQI:
return new DecimalType(aqiResponse.getAqi());
case AQIDESCRIPTION:
return getAqiDescription(aqiResponse.getAqi());
case PM25:
case PM10:
case O3:
case NO2:
case CO:
case SO2:
double value = aqiResponse.getIaqiValue(fields[0]);
return value != -1 ? new DecimalType(value) : UnDefType.UNDEF;
case TEMPERATURE:
double temp = aqiResponse.getIaqiValue("t");
return temp != -1 ? new QuantityType<>(temp, API_TEMPERATURE_UNIT) : UnDefType.UNDEF;
case PRESSURE:
double press = aqiResponse.getIaqiValue("p");
return press != -1 ? new QuantityType<>(press, API_PRESSURE_UNIT) : UnDefType.UNDEF;
case HUMIDITY:
double hum = aqiResponse.getIaqiValue("h");
return hum != -1 ? new QuantityType<>(hum, API_HUMIDITY_UNIT) : UnDefType.UNDEF;
case LOCATIONNAME:
return new StringType(aqiResponse.getCity().getName());
case STATIONID:
return new DecimalType(aqiResponse.getStationId());
case STATIONLOCATION:
return new PointType(aqiResponse.getCity().getGeo());
case OBSERVATIONTIME:
return new DateTimeType(
aqiResponse.getTime().getObservationTime().withZoneSameLocal(timeZoneProvider.getTimeZone()));
case DOMINENTPOL:
return new StringType(aqiResponse.getDominentPol());
case AQI_COLOR:
return getAsHSB(aqiResponse.getAqi());
default:
return UnDefType.UNDEF;
}
}
/**
* Interprets the current aqi value within the ranges;
* Returns AQI in a human readable format
*
* @return
*/
public State getAqiDescription(int index) {
if (index >= 300) {
return HAZARDOUS;
} else if (index >= 201) {
return VERY_UNHEALTHY;
} else if (index >= 151) {
return UNHEALTHY;
} else if (index >= 101) {
return UNHEALTHY_FOR_SENSITIVE;
} else if (index >= 51) {
return MODERATE;
} else if (index > 0) {
return GOOD;
}
return UnDefType.UNDEF;
}
private State getAsHSB(int index) {
State state = getAqiDescription(index);
if (state == HAZARDOUS) {
return HSBType.fromRGB(343, 100, 49);
} else if (state == VERY_UNHEALTHY) {
return HSBType.fromRGB(280, 100, 60);
} else if (state == UNHEALTHY) {
return HSBType.fromRGB(345, 100, 80);
} else if (state == UNHEALTHY_FOR_SENSITIVE) {
return HSBType.fromRGB(30, 80, 100);
} else if (state == MODERATE) {
return HSBType.fromRGB(50, 80, 100);
} else if (state == GOOD) {
return HSBType.fromRGB(160, 100, 60);
}
return UnDefType.UNDEF;
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2020 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.airquality.internal.json;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link AirQualityJsonCity} is responsible for storing
* the "city" node from the waqi.org JSON response
*
* @author Kuba Wolanin - Initial contribution
*/
@NonNullByDefault
public class AirQualityJsonCity {
private String name = "";
private @Nullable String url;
private List<Double> geo = new ArrayList<>();
public String getName() {
return name;
}
public @Nullable String getUrl() {
return url;
}
public String getGeo() {
List<String> list = new ArrayList<>();
geo.forEach(item -> list.add(item.toString()));
return String.join(",", list);
}
}

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2020 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.airquality.internal.json;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link AirQualityJsonData} is responsible for storing
* the "data" node from the waqi.org JSON response
*
* @author Kuba Wolanin - Initial contribution
*/
@NonNullByDefault
public class AirQualityJsonData {
private int aqi;
private int idx;
private @NonNullByDefault({}) AirQualityJsonTime time;
private @NonNullByDefault({}) AirQualityJsonCity city;
private List<Attribute> attributions = new ArrayList<>();
private Map<String, @Nullable AirQualityValue> iaqi = new HashMap<>();
private String dominentpol = "";
/**
* Air Quality Index
*
* @return {Integer}
*/
public int getAqi() {
return aqi;
}
/**
* Measuring Station ID
*
* @return {Integer}
*/
public int getStationId() {
return idx;
}
/**
* Receives "time" node from the "data" object in JSON response
*
* @return {AirQualityJsonTime}
*/
public AirQualityJsonTime getTime() {
return time;
}
/**
* Receives "city" node from the "data" object in JSON response
*
* @return {AirQualityJsonCity}
*/
public AirQualityJsonCity getCity() {
return city;
}
/**
* Collects a list of attributions (vendors making data available)
* and transforms it into readable string.
* Currently displayed in Thing Status description when ONLINE
*
* @return {String}
*/
public String getAttributions() {
List<String> list = new ArrayList<>();
attributions.forEach(item -> list.add(item.getName()));
return "Attributions : " + String.join(", ", list);
}
public String getDominentPol() {
return dominentpol;
}
public double getIaqiValue(String key) {
AirQualityValue result = iaqi.get(key);
if (result != null) {
return result.getValue();
}
return -1;
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2020 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.airquality.internal.json;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* The {@link AirQualityJsonResponse} is the Java class used to map the JSON
* response to the aqicn.org request.
*
* @author Kuba Wolanin - Initial contribution
*/
@NonNullByDefault
public class AirQualityJsonResponse {
public static enum ResponseStatus {
NONE,
@SerializedName("error")
ERROR,
@SerializedName("ok")
OK;
}
private ResponseStatus status = ResponseStatus.NONE;
@SerializedName("data")
private @NonNullByDefault({}) AirQualityJsonData data;
public ResponseStatus getStatus() {
return status;
}
public AirQualityJsonData getData() {
return data;
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2020 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.airquality.internal.json;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* The {@link AirQualityJsonTime} is responsible for storing
* the "time" node from the waqi.org JSON response
*
* @author Kuba Wolanin - Initial contribution
* @author Gaël L'hopital - Use ZonedDateTime instead of Calendar
*/
@NonNullByDefault
public class AirQualityJsonTime {
@SerializedName("s")
private String dateString = "";
@SerializedName("tz")
private String timeZone = "";
private String iso = "";
/**
* Get observation time
*
* @return {ZonedDateTime}
*/
public ZonedDateTime getObservationTime() throws DateTimeParseException {
return ZonedDateTime.parse(iso);
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 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.airquality.internal.json;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Wrapper type around values reported by aqicn index values.
*
* @author Łukasz Dywicki - Initial contribution
*/
@NonNullByDefault
public class AirQualityValue {
@SerializedName("v")
private double value;
public double getValue() {
return value;
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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.airquality.internal.json;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Attribute representation.
*
* @author Łukasz Dywicki - Initial contribution
*/
@NonNullByDefault
public class Attribute {
private @NonNullByDefault({}) String name;
private @Nullable String url;
private @Nullable String logo;
public @Nullable String getUrl() {
return url;
}
public @Nullable String getLogo() {
return logo;
}
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="airquality" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Air Quality Binding</name>
<description>Measure Air Quality Index and details about pollution particles for a given location</description>
<author>Kuba Wolanin</author>
</binding:binding>

View File

@@ -0,0 +1,30 @@
# binding
binding.airquality.name = Extension Air Quality
binding.airquality.description = Indice de qualité de l'air et informations sur la pollution aux particules pour un emplacement donné.
# thing types
thing-type.airquality.aqi.label = Qualité de l'air
thing-type.airquality.aqi.description = Fournit diverses données sur la qualité de l'air du World Air Quality Project. Pour recevoir les données, vous devez créer un compte sur http://aqicn.org/data-platform/token/ pour obtenir votre token API.
channel-type.airquality.aqiLevel.label = Indice
channel-type.airquality.aqiDescription.label = Appréciation
channel-type.airquality.observationTime.label = Heure d'observation
channel-type.airquality.temperature.label = Température
channel-type.airquality.pressure.label = Pression
channel-type.airquality.humidity.label = Humidité
channel-type.airquality.dominentpol.label = Polluant principal
channel-type.airquality.aqiDescription.state.option.GOOD = Bonne
channel-type.airquality.aqiDescription.state.option.MODERATE = Modérée
channel-type.airquality.aqiDescription.state.option.UNHEALTHY_FOR_SENSITIVE = Mauvaise pour les groupes sensibles
channel-type.airquality.aqiDescription.state.option.UNHEALTHY = Mauvaise
channel-type.airquality.aqiDescription.state.option.VERY_UNHEALTHY = Très mauvaise
channel-type.airquality.aqiDescription.state.option.HAZARDOUS = Dangereuse
channel-type.airquality.dominentPol.state.option.pm25 = Particules fines
channel-type.airquality.dominentPol.state.option.pm10 = Particules de poussière
channel-type.airquality.dominentPol.state.option.o3 = Ozone
channel-type.airquality.dominentPol.state.option.no2 = Dioxyde d'azote
channel-type.airquality.dominentPol.state.option.co = Monoxyde de carbone
channel-type.airquality.dominentPol.state.option.so2 = Dioxyde de soufre

View File

@@ -0,0 +1,214 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="airquality"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Air Quality Thing -->
<thing-type id="aqi">
<label>Air Quality</label>
<description>
Provides various air quality data from the World Air Quality Project.
In order to receive the data, you
must register an account on http://aqicn.org/data-platform/token/ and get your API
token.
</description>
<channels>
<channel id="aqiLevel" typeId="aqiLevel"/>
<channel id="aqiColor" typeId="aqiColor"/>
<channel id="aqiDescription" typeId="aqiDescription"/>
<channel id="pm25" typeId="pm25"/>
<channel id="pm10" typeId="pm10"/>
<channel id="o3" typeId="o3"/>
<channel id="no2" typeId="no2"/>
<channel id="co" typeId="co"/>
<channel id="so2" typeId="so2"/>
<channel id="locationName" typeId="locationName"/>
<channel id="stationLocation" typeId="stationLocation"/>
<channel id="stationId" typeId="stationId"/>
<channel id="observationTime" typeId="observationTime"/>
<channel id="temperature" typeId="temperature"/>
<channel id="pressure" typeId="pressure"/>
<channel id="humidity" typeId="humidity"/>
<channel id="dominentpol" typeId="dominentPol"/>
</channels>
<config-description>
<parameter name="apikey" type="text" required="true">
<context>password</context>
<label>API Key</label>
<description>Data-platform token to access the AQIcn.org service</description>
</parameter>
<parameter name="location" type="text" required="false"
pattern="^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)[,]\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$">
<label>Location</label>
<description>Your geo coordinates separated with comma (e.g. "37.8,-122.4").</description>
</parameter>
<parameter name="stationId" type="integer" required="false">
<label>Station ID</label>
<description>Fill only in case you want to receive data from the specific station</description>
<advanced>true</advanced>
</parameter>
<parameter name="refresh" type="integer" min="30" required="false" unit="min">
<label>Refresh Interval</label>
<description>Specifies the refresh interval in minutes.</description>
<advanced>true</advanced>
<default>60</default>
<unitLabel>Minutes</unitLabel>
</parameter>
</config-description>
</thing-type>
<channel-type id="aqiLevel">
<item-type>Number</item-type>
<label>Air Quality Index</label>
<description></description>
<category>Air Quality Index</category>
<state readOnly="true" pattern="%d" min="0" max="500"/>
</channel-type>
<channel-type id="aqiDescription">
<item-type>String</item-type>
<label>AQI Description</label>
<description></description>
<category>AQI Description</category>
<state readOnly="true">
<options>
<option value="GOOD">Good</option>
<option value="MODERATE">Moderate</option>
<option value="UNHEALTHY_FOR_SENSITIVE">Unhealthy for Sensitive Groups</option>
<option value="UNHEALTHY">Unhealthy</option>
<option value="VERY_UNHEALTHY">Very Unhealthy</option>
<option value="HAZARDOUS">Hazardous</option>
</options>
</state>
</channel-type>
<channel-type id="pm25">
<item-type>Number</item-type>
<label>PM2.5</label>
<description>Fine particles pollution level</description>
<category>PM2.5</category>
<state readOnly="true" pattern="%d" min="0" max="500"/>
</channel-type>
<channel-type id="pm10">
<item-type>Number</item-type>
<label>PM10</label>
<description>Coarse dust particles pollution level</description>
<category>PM10</category>
<state readOnly="true" pattern="%d" min="0" max="500"/>
</channel-type>
<channel-type id="o3">
<item-type>Number</item-type>
<label>O3</label>
<description>Ozone level</description>
<category>O3</category>
<state readOnly="true" pattern="%.1f" min="0" max="500"/>
</channel-type>
<channel-type id="no2">
<item-type>Number</item-type>
<label>NO2</label>
<description>Nitrogen dioxide level</description>
<category>NO2</category>
<state readOnly="true" pattern="%.1f" min="0" max="500"/>
</channel-type>
<channel-type id="co">
<item-type>Number</item-type>
<label>CO</label>
<description>Carbon monoxide level</description>
<category>CO</category>
<state readOnly="true" pattern="%.1f" min="0" max="500"/>
</channel-type>
<channel-type id="so2">
<item-type>Number</item-type>
<label>SO2</label>
<description>Sulfur dioxide level</description>
<category>SO2</category>
<state readOnly="true" pattern="%.1f"/>
</channel-type>
<channel-type id="locationName" advanced="true">
<item-type>String</item-type>
<label>Location</label>
<description>Nearest measuring station location</description>
<category>Location</category>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="stationLocation" advanced="true">
<item-type>Location</item-type>
<label>Station Location</label>
<description>Location of the measuring station</description>
<category>Station Location</category>
<state readOnly="true" pattern="%2$s°N,%3$s°W"/>
</channel-type>
<channel-type id="stationId" advanced="true">
<item-type>Number</item-type>
<label>Station ID</label>
<description>Unique measuring station ID</description>
<category>Station ID</category>
<state readOnly="true" pattern="%d"/>
</channel-type>
<channel-type id="observationTime" advanced="true">
<item-type>DateTime</item-type>
<label>Observation Time</label>
<description>Observation date and time</description>
<category>Observation time</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="temperature" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Temperature</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="pressure" advanced="true">
<item-type>Number:Pressure</item-type>
<label>Pressure</label>
<description>Current Pressure</description>
<category>Pressure</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="humidity" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Current humidity</description>
<category>Humidity</category>
<state readOnly="true" min="0" max="100" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="dominentPol">
<item-type>String</item-type>
<label>Dominent Polutor</label>
<state readOnly="true">
<options>
<option value="pm25">Fine particles</option>
<option value="pm10">Coarse dust particles</option>
<option value="o3">Ozone</option>
<option value="no2">Nitrogen Dioxide</option>
<option value="co">Carbon Monoxide</option>
<option value="so2">Sulfur Dioxide</option>
</options>
</state>
</channel-type>
<channel-type id="aqiColor" advanced="true">
<item-type>Color</item-type>
<label>AQI Color</label>
<description>Color associated to given AQI Index.</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>