Air-q device any good

Finally some results in the area of NO2 / O3, this was hard stuff to achieve.

Quick recap:

  • our O3 sensor has 1:1 cross sensitivity to NO2 and 1:2 negative cross sensitivity to H2S
  • NO2 and O3 are usually correlated, see quick explaination below

When the sun rise, and with maximum effect at noon, UV light hit the VOC and NOx molecules, this creates Ozone as a chemical reaction. I am not an expert on this topic, but there’s plenty of analisys you can google, basically (copy pasted with no shame):

It is well established that the inter-conversion of O3, NO
and NO2 under atmospheric conditions is generally
dominated by the following reactions:
NO2 + hv = NO + O (1)
O + O2 = O3 + M (2)
O3 + NO = NO2 + O2 (3)
M (usually N2 or O2) represents a molecule that absorbs
the excess vibrational energy and thereby stabilizes the O3
molecule formed. hv represents the energy of a photon
(with a wavelength of < 424 nm) and O is an active
monoatomic oxygen molecule. These equations form a
cycle with no net chemistry i.e. the overall effect of
reaction (2) is the reverse of reaction (1). These reactions
therefore represent a closed system in which the NOx (NO
and NO2) components and the OX (O3 and NO2)
components relate separately. During daytime hours, NO,
NO2 and O3 are typically equilibrated on a timescale of a
few minutes. This is known as a photostationary state.

We trust whatever is written above :slight_smile:

What I noticed is O3 + NO2 (what basically out O3 sensor gives us) is more or less constant, that makes sense, as for the explaination above.

Our NO2 sensors has a lot less cross sensitivities, what I noticed is it has a huge relation with temperature, to be more specific, the relation is not with the absolute value of T, instead it does reacts to how fast the temperature changes, graphically speaking to the slope, what in math is called a derivative.

Even a really small change in temperature does fuck up our data, see here an example:

See here another example

It is clear that that data is pure bullshit, but the issue is because we are reading really low values compared to what the sensor is meant to do (see the datasheet some post above), we can still try to get something useful with a compensation in temperature and with some sort of Exponential smoothing - Wikipedia and averaging.

See what happens when I put the device outside, with a drastic climate change:

Here is how NO2 corrections are handled:

And Ozone:

Here you have a comparison of the achieved result with an official station 10km away

And here the math:

sensor:

  - platform: derivative
    source: sensor.airq_temperature
    name: "Derivata T AirQ"
    time_window: "00:01:00"
    unit_time: h

  - platform: statistics
    name: "Sum Derivative T AirQ 15"
    entity_id: sensor.derivata_t_airq
    state_characteristic: sum
    max_age:
      minutes: 15
    sampling_size: 15 
  - platform: statistics
    name: "Sum Derivative T AirQ 30"
    entity_id: sensor.derivata_t_airq
    state_characteristic: sum
    max_age:
      minutes: 30
    sampling_size: 30 
  - platform: statistics
    name: "Sum Derivative T AirQ 1h"
    entity_id: sensor.derivata_t_airq
    state_characteristic: sum
    max_age:
      minutes: 60
    sampling_size: 60 
  - platform: statistics
    name: "Sum Derivative T AirQ 2h"
    entity_id: sensor.derivata_t_airq
    state_characteristic: sum
    max_age:
      minutes: 120
    sampling_size: 120 

  - platform: filter
    name: "filtered airq no2 avg"
    entity_id: sensor.airq_compensated_no2
    filters:
      - filter: lowpass
        time_constant: 45
      - filter: time_simple_moving_average
        window_size: "00:10"

  - platform: filter
    name: "filtered airq o3 avg"
    entity_id: sensor.airq_ozone
    filters:
      - filter: lowpass
        time_constant: 30
      - filter: time_simple_moving_average
        window_size: "00:15"

  - platform: statistics
    name: "distance 95 no2"
    entity_id: sensor.airq_no2
    state_characteristic: distance_95_percent_of_values
    max_age:
      minutes: 10080
    sampling_size: 10080   
  - platform: statistics
    name: "min multiplied no2"
    entity_id: sensor.airq_compensated_multiplied_no2
    state_characteristic: value_min
    max_age:
      minutes: 10080
    sampling_size: 10080  

  - platform: statistics
    name: "min o3"
    entity_id: sensor.filtered_airq_o3_avg
    state_characteristic: value_min
    max_age:
      minutes: 10080
    sampling_size: 10080 
  - platform: statistics
    name: "max o3"
    entity_id: sensor.filtered_airq_o3_avg
    state_characteristic: value_max
    max_age:
      minutes: 10080
    sampling_size: 10080 



template:

  - trigger:
      - platform: time_pattern
        minutes: "/1"
        seconds: "1"   
    sensor:
    - name: "AirQ Compensated NO2"
      unit_of_measurement: "µg/m³"
      state: "{{ states('sensor.airq_no2') | float(0) - 0.1 * states('sensor.filtered_airq_o3_avg') | float(0) + 0.06 * states('sensor.sum_derivative_t_airq_15') | float(0) + 0.12 * states('sensor.sum_derivative_t_airq_30') | float(0) + 0.03 * states('sensor.sum_derivative_t_airq_1h') | float(0) + 0.02 * states('sensor.sum_derivative_t_airq_2h') | float(0) }}"

  - sensor:
    - name: "AirQ NO2 multiplier"
      state: "{{ ( states('sensor.max_o3') | float - states('sensor.min_o3') | float ) / states('sensor.distance_95_no2') | float }}"

  - sensor:
    - name: "AirQ Compensated multiplied NO2"
      unit_of_measurement: "µg/m³"
      state: "{{ states('sensor.airq_no2_multiplier') | float * states('sensor.filtered_airq_no2_avg') | float }}"

  - sensor:
    - name: "AirQ Compensated Calibrated NO2"
      unit_of_measurement: "µg/m³"
      state: "{{ states('sensor.airq_compensated_multiplied_no2') | float - states('sensor.min_multiplied_no2') | float  }}"


  - trigger:
      - platform: time_pattern
        minutes: "/1"
        seconds: "5"   
    sensor:
    - name: "AirQ Compensated Calibrated H2S"
      unit_of_measurement: "µg/m³"
      state: "{{ max( 0.6 * max( states('sensor.airq_h2s') | float(0), 0.1) - 0.04 * (20 - states('sensor.airq_temperature') | float) + 0.18 * 0.6 * states('sensor.average_1h_o3') | float(0) + 0.2 * 0.6 * states('sensor.average_1h_no2') | float(0) - 12, 0.1) }}"

  - trigger:
      - platform: time_pattern
        minutes: "/1"
        seconds: "10"   
    sensor:
    - name: "AirQ Compensated Calibrated O3"
      unit_of_measurement: "µg/m³"
      state: "{{ max( max( states('sensor.filtered_airq_o3_avg') | float(0), 0.1) - max( states('sensor.airq_compensated_calibrated_no2') | float(0), 0.1 )  + 0.5 * max( states('sensor.airq_compensated_calibrated_h2s') | float(0), 0.1) - max( states('sensor.min_03') | float(0), 0.1 ), 0.1) }}"