Skip to content
76 changes: 47 additions & 29 deletions bundles/org.openhab.binding.airgradient/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ If you don't specify any path on the server, the binding will behave as if the h

The binding will adapt to the content type of the returned content to support different formats for getting data both from local and cloud installations.

| Name | Hostname | Content-Type | Parser |
|-------------------|-----------------------------------------------------------------|------------------------------|--------|
| API | Hostnames without any path (e.g., `https://api.airgradient.com/`) | application/json | JSON parser for the AirGradient API, correct paths will be appended to the calls |
| Local OpenMetrics | Hostnames with path (e.g., `http://192.168.x.x/metrics`) | application/openmetrics-text | OpenMetrics parser |
| Local Web | Hostnames with path (e.g., `http://192.168.x.x/measures/current`) | application/json | JSON parser for the AirGradient API, as if you returned the value of sendToServer() payload |
| Name | Hostname | Content-Type | Parser |
|-------------------|-------------------------------------------------------------------|------------------------------|---------------------------------------------------------------------------------------------------------|
| API | Hostnames without any path (e.g., `https://api.airgradient.com/`) | application/json | JSON parser for the AirGradient API, correct paths will be appended to the calls |
| Local OpenMetrics | Hostnames with path (e.g., `http://192.168.x.x/metrics`) | application/openmetrics-text | OpenMetrics parser |
| Local Web | Hostnames with path (e.g., `http://192.168.x.x/measures/current`) | application/json | JSON parser for the AirGradient API, as if you returned the value of sendToServer() payload |
| Local Prometheus | Hostnames with path (e.g., `http://192.168.x.x/measures`) | text/plain | Prometheus parser for [Prometheus format](https://prometheus.io/docs/instrumenting/exposition_formats/) |

### AirGradient API
Expand All @@ -51,17 +51,17 @@ To add a location, you need to know the location ID. To get the location ID, you

### `API` Thing Configuration

| Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|---------------------------------------|------------------------------|----------|----------|
| token | text | Token to access the device | N/A | yes | no |
| Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|---------------------------------------|--------------------------------|----------|----------|
| token | text | Token to access the device | N/A | yes | no |
| hostname | text | Hostname or IP address of the API | `https://api.airgradient.com/` | no | yes |
| refreshInterval | integer | Interval the device is polled in sec. | 600 | no | yes |
| refreshInterval | integer | Interval the device is polled in sec. | 600 | no | yes |

### `Location` Thing Configuration

| Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|-------------------------------------------------------------------|---------|----------|----------|
| location | text | A number identifying the location id in the AirGradient Dashboard | N/A | yes | no |
| Name | Type | Description | Default | Required | Advanced |
|----------|------|-------------------------------------------------------------------|---------|----------|----------|
| location | text | A number identifying the location id in the AirGradient Dashboard | N/A | yes | no |

## Channels

Expand All @@ -70,7 +70,7 @@ For more information about the data in the channels, please refer to the [models
| Channel | Type | Read/Write | Description |
|--------------------|----------------------|------------|----------------------------------------------------------------------------------|
| pm01 | Number:Density | Read | Particulate Matter 1 (0.001mm) |
| pm02 | Number:Density | Read | Particulate Matter 2 (0.002mm) |
| pm02 | Number:Density | Read | Particulate Matter 2.5 (0.0025mm) |
| pm10 | Number:Density | Read | Particulate Matter 10 (0.01mm) |
| pm003-count | Number:Dimensionless | Read | The number of particles with a diameter beyond 0.3 microns in 1 deciliter of air |
| rco2 | Number:Density | Read | Carbon dioxide PPM |
Expand All @@ -85,21 +85,39 @@ For more information about the data in the channels, please refer to the [models
Some configuration channels are only available for local devices (for cloud devices use the AirGradient dashboard to configure these instead).
These configuration settings needs AirGradient firmware on the sensor of version 3.1.1 or later.

| Channel | Type | Read/Write | Description |
|-----------------------|----------------------|------------|----------------------------------------------------------------------------------|
| country-code | String | Read/Write | The ALPHA-2 Country code used for the device |
| pm-standard | String | Read/Write | Standard used for Parts per Million measurements (us-aqi or ugm3) |
| abc-days | Number:Days | Read/Write | Co2 calibration automatic baseline calibration days |
| tvoc-learning-offset | Number:Dimensionless | Read/Write | Time constant of long-term estimator for offset. |
| nox-learning-offset | Number:Dimensionless | Read/Write | Time constant of long-term estimator for offset. |
| mqtt-broker-url | String | Read/Write | MQTT Broker URL |
| temperature-unit | String | Read/Write | Temperature unit used on the display |
| configuration-control | String | Read/Write | Where the unit is configured from (local/cloud/both) |
| post-to-cloud | Switch | Read/Write | Send data to the AirGradient cloud |
| led-bar-brightness | Number:Dimensionless | Read/Write | Brightness of the LED bar |
| display-brightness | Number:Dimensionless | Read/Write | Brightness of the display |
| model | String | Read/Write | The model of the device (can be changed e.g. if you change sensors) |
| led-bar-test | String | Write | Trigger test of LED bar |
| Channel | Type | Read/Write | Description |
|-----------------------|----------------------|------------|---------------------------------------------------------------------|
| country-code | String | Read/Write | The ALPHA-2 Country code used for the device |
| pm-standard | String | Read/Write | Standard used for Parts per Million measurements (us-aqi or ugm3) |
| abc-days | Number:Days | Read/Write | Co2 calibration automatic baseline calibration days |
| tvoc-learning-offset | Number:Dimensionless | Read/Write | Time constant of long-term estimator for offset. |
| nox-learning-offset | Number:Dimensionless | Read/Write | Time constant of long-term estimator for offset. |
| mqtt-broker-url | String | Read/Write | MQTT Broker URL |
| temperature-unit | String | Read/Write | Temperature unit used on the display |
| configuration-control | String | Read/Write | Where the unit is configured from (local/cloud/both) |
| post-to-cloud | Switch | Read/Write | Send data to the AirGradient cloud |
| led-bar-brightness | Number:Dimensionless | Read/Write | Brightness of the LED bar |
| display-brightness | Number:Dimensionless | Read/Write | Brightness of the display |
| model | String | Read/Write | The model of the device (can be changed e.g. if you change sensors) |
| led-bar-test | String | Write | Trigger test of LED bar |

The following additional measurement channels are added dynamically, depending on the device model and firmware version.

| Channel | Type | Read/Write | Description |
|------------------|----------------------|------------|---------------------------------------------------------------|
| pm01-standard | Number:Density | Read | PM1.0 concentration (standard particle) |
| pm02-standard | Number:Density | Read | PM2.5 concentration (standard particle) |
| pm10-standard | Number:Density | Read | PM10 concentration (standard particle) |
| pm005-count | Number:Dimensionless | Read | Particle count for particles >= 0.5 microns per deciliter air |
| pm01-count | Number:Dimensionless | Read | Particle count for particles >= 1.0 microns per deciliter air |
| pm02-count | Number:Dimensionless | Read | Particle count for particles >= 2.5 microns per deciliter air |
| pm50-count | Number:Dimensionless | Read | Particle count for particles >= 5.0 microns per deciliter air |
| pm10-count | Number:Dimensionless | Read | Particle count for particles >= 10 microns per deciliter air |
| pm02-compensated | Number:Density | Read | PM2.5 concentration with correction applied |
| tvoc-index | Number:Dimensionless | Read | TVOC index value |
| tvoc-raw | Number:Dimensionless | Read | Raw TVOC value |
| nox-index | Number:Dimensionless | Read | NOx index value |
| nox-raw | Number:Dimensionless | Read | Raw NOx value |

## Full Example

Expand All @@ -114,6 +132,6 @@ Bridge airgradient:airgradient-api:home "My Home" [ token="abc123...." ] {
### Item Configuration

```java
Number:Density AirGradient_Location_PM2 "%.0f kg/m³" <density> {channel="airgradient:location:654321:pm2"}"
Number:Density AirGradient_Location_PM2 "%.0f kg/m³" <density> {channel="airgradient:location:654321:pm02"}"
Number:Temperature AirGradient_Location_PM2 "Temperature [%.1f °C]" <temperature> {channel="airgradient:location:654321:atmp"}"
```
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ public class AirGradientBindingConstants {
public static final String CHANNEL_DISPLAY_BRIGHTNESS = "display-brightness";
public static final String CHANNEL_MODEL = "model";
public static final String CHANNEL_LED_BAR_TEST = "led-bar-test";
public static final String CHANNEL_PM01_STANDARD = "pm01-standard";
public static final String CHANNEL_PM02_STANDARD = "pm02-standard";
public static final String CHANNEL_PM10_STANDARD = "pm10-standard";
public static final String CHANNEL_PM005_COUNT = "pm005-count";
public static final String CHANNEL_PM01_COUNT = "pm01-count";
public static final String CHANNEL_PM02_COUNT = "pm02-count";
public static final String CHANNEL_PM50_COUNT = "pm50-count";
public static final String CHANNEL_PM10_COUNT = "pm10-count";
public static final String CHANNEL_PM02_COMPENSATED = "pm02-compensated";
public static final String CHANNEL_TVOC_INDEX = "tvoc-index";
public static final String CHANNEL_TVOC_RAW = "tvoc-raw";
public static final String CHANNEL_NOX_INDEX = "nox-index";
public static final String CHANNEL_NOX_RAW = "nox-raw";

// List of all properties
public static final String PROPERTY_NAME = "name";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,15 @@
*/
package org.openhab.binding.airgradient.internal.discovery;

import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.BACKGROUND_DISCOVERY;
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CONFIG_LOCATION;
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.PROPERTY_NAME;
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.SEARCH_TIME;
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.THING_TYPE_LOCATION;
import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.*;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.airgradient.internal.communication.AirGradientCommunicationException;
import org.openhab.binding.airgradient.internal.handler.AirGradientAPIHandler;
Expand Down Expand Up @@ -128,6 +124,6 @@ protected void startScan() {
}

private AirGradientAPIHandler getApiHandler() {
return (@NonNull AirGradientAPIHandler) getThingHandler();
return Objects.requireNonNull(getThingHandler());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class AirGradientLocalHandler extends BaseThingHandler {

private @NonNullByDefault({}) RemoteAPIController apiController = null;
private @NonNullByDefault({}) AirGradientAPIConfiguration apiConfig = null;
private @Nullable String cachedDeviceSignature;

public AirGradientLocalHandler(Thing thing, HttpClient httpClient) {
super(thing);
Expand Down Expand Up @@ -152,7 +153,7 @@ public void initialize() {
"Need to set hostname to a valid URL. Refresh interval needs to be a positive integer.");
return;
}

cachedDeviceSignature = null;
apiController = new RemoteAPIController(httpClient, gson, apiConfig);

// set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
Expand All @@ -175,6 +176,12 @@ protected void pollingCode() {
}

Measure measure = measures.get(0);
ThingBuilder measurementBuilder = DynamicChannelHelper.updateThingWithMeasurementChannels(thing, null,
this::editThing, measure);
if (measurementBuilder != null) {
updateThing(measurementBuilder.build());
}

updateProperties(MeasureHelper.createProperties(measure));
Map<String, State> states = MeasureHelper.createStates(measure);
for (Map.Entry<String, State> entry : states.entrySet()) {
Expand All @@ -183,12 +190,18 @@ protected void pollingCode() {
}
}

String deviceSignature = DynamicChannelHelper
.getDynamicChannelCapabilitySignature(measure.getFirmwareVersion(), measure.getModel());
boolean deviceSignatureChanged = deviceSignature == null || !deviceSignature.equals(cachedDeviceSignature);
LocalConfiguration localConfig = apiController.getConfig();
if (localConfig != null) {
// If we are able to read config, we add config channels
ThingBuilder builder = DynamicChannelHelper.updateThingWithConfigurationChannels(thing, editThing());
updateThing(builder.build());

if (deviceSignatureChanged) {
ThingBuilder builder = DynamicChannelHelper.updateThingWithConfigurationChannels(thing, null,
this::editThing, localConfig);
if (builder != null) {
updateThing(builder.build());
}
}
updateProperties(ConfigurationHelper.createProperties(localConfig));
Map<String, State> configStates = ConfigurationHelper.createStates(localConfig);
for (Map.Entry<String, State> entry : configStates.entrySet()) {
Expand All @@ -197,6 +210,7 @@ protected void pollingCode() {
}
}
}
cachedDeviceSignature = deviceSignature;

} catch (AirGradientCommunicationException agce) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, agce.getMessage());
Expand Down Expand Up @@ -226,4 +240,8 @@ public void dispose() {
protected void setConfiguration(AirGradientAPIConfiguration config) {
this.apiConfig = config;
}

protected void setApiController(RemoteAPIController apiController) {
this.apiController = apiController;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
Expand Down Expand Up @@ -135,6 +136,12 @@ public String getLocationId() {
}

public void setMeasurment(Measure measure) {
ThingBuilder builder = DynamicChannelHelper.updateThingWithMeasurementChannels(thing, null, this::editThing,
measure);
if (builder != null) {
updateThing(builder.build());
}

updateProperties(MeasureHelper.createProperties(measure));
Map<String, State> states = MeasureHelper.createStates(measure);
for (Map.Entry<String, State> entry : states.entrySet()) {
Expand Down
Loading