AirGradient Forum

Got original Pro DIY kit connected to Home Assistant via REST api

Apologies if this is well-known, but I was struggling with using the normal HA integration as I have the Pro DIY and can’t update its firmware with anything modern (nor for some reason connect to it directly with http). Though I’d prefer the normal integration, I had good luck getting all of its relevant data out via adding a REST sensor and extracting out all of the data to entities. This went straight into configuration.yaml, replacing with my actual location and token in the resource field:

sensor:
  - platform: rest
    name: "AirGradient Cloud Bedroom Raw"
    resource: "https://api.airgradient.com/public/api/v1/locations/<######>/measures/current?token=<xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx>"
    method: GET
    scan_interval: 60
    timeout: 15
    value_template: "{{ value_json.timestamp }}"
    json_attributes:
      - locationId
      - locationName
      - locationType
      - pm02
      - pm02_corrected
      - atmp
      - atmp_corrected
      - rhum
      - rhum_corrected
      - rco2
      - rco2_corrected
      - tvoc
      - tvocIndex
      - wifi
      - serialno
      - model


template:
  - sensor:
      # ---- Identity ----
      - name: "AirGradient Bedroom Model"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','model') }}"

      - name: "AirGradient Bedroom Serial"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','serialno') }}"

      # ---- CO2 ----
      - name: "AirGradient Bedroom CO2"
        unit_of_measurement: "ppm"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','rco2') }}"

      - name: "AirGradient Bedroom CO2 Corrected"
        unit_of_measurement: "ppm"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','rco2_corrected') }}"

      # ---- Particulates ----
      - name: "AirGradient Bedroom PM2.5"
        unit_of_measurement: "µg/m³"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','pm02') }}"

      - name: "AirGradient Bedroom PM2.5 Corrected"
        unit_of_measurement: "µg/m³"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','pm02_corrected') }}"

      # ---- Temperature ----
      - name: "AirGradient Bedroom Temperature"
        unit_of_measurement: "°C"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','atmp') }}"

      - name: "AirGradient Bedroom Temperature Corrected"
        unit_of_measurement: "°C"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','atmp_corrected') }}"

      # ---- Humidity ----
      - name: "AirGradient Bedroom Humidity"
        unit_of_measurement: "%"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','rhum') }}"

      - name: "AirGradient Bedroom Humidity Corrected"
        unit_of_measurement: "%"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','rhum_corrected') }}"

      # ---- VOC ----
      - name: "AirGradient Bedroom TVOC"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','tvoc') }}"

      - name: "AirGradient Bedroom TVOC Index"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','tvocIndex') }}"

      # ---- Wi-Fi signal ----
      - name: "AirGradient Bedroom WiFi RSSI"
        unit_of_measurement: "dBm"
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','wifi') }}"

I removed any fields (latitude, longitude, other data that might only exist in newer devices) that show up as null in the raw data for me.

Let me know if you’ve already discovered a better way. I’ll pull this out into an include so it’s not clogging up my base configuration.yaml, but this gives you the idea.

Very nice. I might try to use this same model to retrieve metrics for my outdoor Max that uses cellular so the native integration doesn’t work for it at the moment.

At one point there was a beta firmware for the earlier boards that worked with the HA integration, but I can’t find the link now and it doesn’t seem like it was under active development.

Since you are a proficient HA user, if you have ESPHome experience you could try out my configurations for this board using that and it would natively report to HA without needing this YAML config.

Hardly a proficient user yet (just started last week), but hoping to get it all figured out. In that vein, I pulled all of the above into a package to separate it all from the main config, and moved the API Token into secrets.yaml for security purposes, so if you’re interested in actually using this, hit me up and I can update the post with that version!

Cheers.

Yes please. I would like to see what you have

Great. Hopefully this formats correctly. If anyone else uses this, I’ll probably push this to a git repo. Rename as desired. I called mine “Bedroom” because that’s literally where it is, and I assumed I’d have multiple. You could simplify if you want.

In /homeassistant/configuration.yaml, make sure you have this, or the equivalent

homeassistant:
  packages: !include_dir_named packages

In /homeasisstant/secrets.yaml (replace ###### with your location, and the token with your actual token):

airgradient_bedroom_resource: "https://api.airgradient.com/public/api/v1/locations/######/measures/current?token=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

In /homeassistant/packages/airgradient_bedroom.yaml (or whatever you want to call this instance):

# AirGradient (Cloud API) - Bedroom
# Device model: I-1DIY
# Location ID: ###### (just a comment for my convenience)

sensor:
  - platform: rest
    name: "AirGradient Cloud Bedroom Raw"
    resource: !secret airgradient_bedroom_resource
    method: GET
    scan_interval: 60
    timeout: 15
    value_template: "{{ value_json.timestamp }}"
    json_attributes:
      - locationId
      - locationName
      - locationType
      - pm02
      - pm02_corrected
      - atmp
      - atmp_corrected
      - rhum
      - rhum_corrected
      - rco2
      - rco2_corrected
      - tvoc
      - tvocIndex
      - wifi
      - serialno
      - model

template:
  - sensor:
      - name: "AirGradient Bedroom CO2"
        unit_of_measurement: "ppm"
        device_class: carbon_dioxide
        state_class: measurement
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','rco2_corrected') | int }}"

      - name: "AirGradient Bedroom PM2.5"
        unit_of_measurement: "µg/m³"
        device_class: pm25
        state_class: measurement
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','pm02_corrected') | float }}"

      - name: "AirGradient Bedroom Temperature"
        unit_of_measurement: "°C"
        device_class: temperature
        state_class: measurement
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','atmp_corrected') | float }}"

      - name: "AirGradient Bedroom Humidity"
        unit_of_measurement: "%"
        device_class: humidity
        state_class: measurement
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','rhum_corrected') | float }}"

      - name: "AirGradient Bedroom TVOC Index"
        state_class: measurement
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','tvocIndex') | int }}"

      - name: "AirGradient Bedroom WiFi RSSI"
        unit_of_measurement: "dBm"
        device_class: signal_strength
        state_class: measurement
        state: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','wifi') | int }}"
        
      - name: "AirGradient Bedroom Air Quality Score"
        unique_id: airgradient_bedroom_air_quality_score
        unit_of_measurement: "score"
        state_class: measurement
        availability: >
          {{ has_value('sensor.airgradient_cloud_bedroom_raw')
             and state_attr('sensor.airgradient_cloud_bedroom_raw','rco2_corrected') is not none
             and state_attr('sensor.airgradient_cloud_bedroom_raw','pm02_corrected') is not none
             and state_attr('sensor.airgradient_cloud_bedroom_raw','tvocIndex') is not none }}
        state: >
          {# Inputs #}
          {% set co2  = state_attr('sensor.airgradient_cloud_bedroom_raw','rco2_corrected') | float %}
          {% set pm25 = state_attr('sensor.airgradient_cloud_bedroom_raw','pm02_corrected') | float %}
          {% set tvoc = state_attr('sensor.airgradient_cloud_bedroom_raw','tvocIndex') | float %}

          {# Penalty functions: 0 = great, higher = worse #}
          {% set co2_pen =
            0 if co2 <= 800 else
            10 if co2 <= 1000 else
            25 if co2 <= 1200 else
            45 if co2 <= 1600 else
            60 if co2 <= 2000 else
            80
          %}

          {% set pm_pen =
            0 if pm25 <= 5 else
            10 if pm25 <= 12 else
            30 if pm25 <= 35 else
            45 if pm25 <= 55 else
            80
          %}

          {% set tvoc_pen =
            0 if tvoc <= 100 else
            10 if tvoc <= 150 else
            20 if tvoc <= 200 else
            35 if tvoc <= 300 else
            50 if tvoc <= 400 else
            65
          %}

          {# Combine #}
          {% set score = 100 - (co2_pen + pm_pen + tvoc_pen) %}
          {{ [0, [100, score] | min] | max | int }}

        attributes:
          category: >
            {% set s = this.state | int(0) %}
            {{ 'excellent' if s >= 85 else
               'good' if s >= 70 else
               'fair' if s >= 50 else
               'poor' }}
          co2_ppm: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','rco2_corrected') }}"
          pm25_ugm3: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','pm02_corrected') }}"
          tvoc_index: "{{ state_attr('sensor.airgradient_cloud_bedroom_raw','tvocIndex') }}"
        
  - binary_sensor:
      - name: "AirGradient Bedroom Air Quality Out of Bounds"
        unique_id: airgradient_bedroom_air_quality_out_of_bounds
        availability: "{{ has_value('sensor.airgradient_bedroom_air_quality_score') }}"
        state: >
          {% set score = states('sensor.airgradient_bedroom_air_quality_score') | int(100) %}
          {% set co2  = state_attr('sensor.airgradient_cloud_bedroom_raw','rco2_corrected') | float(0) %}
          {% set pm25 = state_attr('sensor.airgradient_cloud_bedroom_raw','pm02_corrected') | float(0) %}

          {{ score < 50 or co2 > 1500 or pm25 > 35 }}
        attributes:
          reason: >
            {% set score = states('sensor.airgradient_bedroom_air_quality_score') | int(100) %}
            {% set co2  = state_attr('sensor.airgradient_cloud_bedroom_raw','rco2_corrected') | float(0) %}
            {% set pm25 = state_attr('sensor.airgradient_cloud_bedroom_raw','pm02_corrected') | float(0) %}
            {% if pm25 > 35 %}pm2_5_high
            {% elif co2 > 1500 %}co2_high
            {% elif score < 50 %}composite_score_low
            {% else %}ok
            {% endif %}
            
            
automation:
  - id: airgradient_bedroom_air_quality_warn
    alias: "AirGradient Bedroom - Air quality warning"
    mode: single
    trigger:
      - platform: state
        entity_id: binary_sensor.airgradient_bedroom_air_quality_out_of_bounds
        to: "on"
        for: "00:10:00"
    action:
      - service: notify.notify
        data:
          title: "Bedroom air quality warning"
          message: >
            Air quality is out of bounds ({{ state_attr('binary_sensor.airgradient_bedroom_air_quality_out_of_bounds','reason') }}).
            Score={{ states('sensor.airgradient_bedroom_air_quality_score') }},
            CO₂={{ state_attr('sensor.airgradient_cloud_bedroom_raw','rco2_corrected') }} ppm,
            PM2.5={{ state_attr('sensor.airgradient_cloud_bedroom_raw','pm02_corrected') }} µg/m³,
            TVOC Index={{ state_attr('sensor.airgradient_cloud_bedroom_raw','tvocIndex') }}.

      - service: persistent_notification.create
        data:
          title: "Bedroom air quality warning"
          message: >
            Air quality has been out of bounds for 10 minutes.
            Score={{ states('sensor.airgradient_bedroom_air_quality_score') }},
            CO₂={{ state_attr('sensor.airgradient_cloud_bedroom_raw','rco2_corrected') }} ppm,
            PM2.5={{ state_attr('sensor.airgradient_cloud_bedroom_raw','pm02_corrected') }} µg/m³,
            TVOC Index={{ state_attr('sensor.airgradient_cloud_bedroom_raw','tvocIndex') }}.

  - id: airgradient_bedroom_air_quality_recover
    alias: "AirGradient Bedroom - Air quality recovered"
    mode: single
    trigger:
      - platform: state
        entity_id: binary_sensor.airgradient_bedroom_air_quality_out_of_bounds
        to: "off"
        for: "00:10:00"
    action:
      - service: notify.notify
        data:
          title: "Bedroom air quality back to normal"
          message: >
            Air quality has recovered.
            Score={{ states('sensor.airgradient_bedroom_air_quality_score') }},
            CO₂={{ state_attr('sensor.airgradient_cloud_bedroom_raw','rco2_corrected') }} ppm,
            PM2.5={{ state_attr('sensor.airgradient_cloud_bedroom_raw','pm02_corrected') }} µg/m³,
            TVOC Index={{ state_attr('sensor.airgradient_cloud_bedroom_raw','tvocIndex') }}.
            

Oh yeah, I guess I added the automation to that so I would get a phone notification if the air quality ever got out of bounds in my room, or that it had returned to normal levels afterwards. That’s already fired for me once!