DEV Community

Cover image for HEPA Filter Tech Explained: What Developers Building Air Quality Apps Should Know
Mehul Patel
Mehul Patel

Posted on

HEPA Filter Tech Explained: What Developers Building Air Quality Apps Should Know

If you're building an air quality monitoring app, integrating IoT sensors, or designing smart building dashboards - you've probably come across the term HEPA filter. But do you know what's actually happening at the physics level? And more importantly, how does it affect the data your sensors report?

This article breaks down HEPA filter mechanics from a developer's perspective, shows you how to read PM2.5 sensor data in Python, and explains why commercial-grade HEPA systems (like those inside modern automatic hand dryers) are becoming a data source worth integrating into your indoor air quality apps.

What Is a HEPA Filter, Really?

HEPA stands for High-Efficiency Particulate Air. It's not a brand - it's a performance standard.

To qualify as a true HEPA filter, a filtration medium must capture 99.97% of particles at 0.3 micrometers (µm) in diameter. Why 0.3µm specifically? Because that's the Most Penetrating Particle Size (MPPS) - the size that's hardest to trap.

Here's the counterintuitive part: particles larger than 0.3µm are actually easier to catch (they get stuck by inertia or interception), and particles smaller than 0.3µm are also easier to catch (they move erratically via Brownian motion and collide with filter fibers). The 0.3µm particle is the "Goldilocks problem" - it's the hardest adversary.

Particle capture mechanisms in HEPA:

Size > 1µm   → Inertial Impaction  (particle slams into fiber)
Size ~0.3µm  → Interception        (particle grazes fiber edge)  ← MPPS
Size < 0.1µm → Diffusion           (Brownian motion causes collision)
Enter fullscreen mode Exit fullscreen mode

The Particle Size Spectrum - What Your Sensor Is Actually Measuring

Before writing a single line of sensor code, you need to understand what you're measuring:

Most low-cost sensors (PMS5003, SDS011, Plantower series) report PM1.0, PM2.5, and PM10. A HEPA-equipped device theoretically eliminates most of what these sensors measure.

Reading PM2.5 Data in Python - Getting Started

Here's a minimal working example to read from a PMS5003 sensor (one of the most popular sensors used in air quality projects) via UART on a Raspberry Pi:

import serial
import struct
import time

def read_pms5003(port='/dev/ttyS0', baudrate=9600):
    """
    Read PM1.0, PM2.5, PM10 from PMS5003 sensor.
    Returns dict with particle concentrations in µg/m³
    """
    ser = serial.Serial(port, baudrate=baudrate, timeout=2)

    # PMS5003 sends 32-byte frames starting with 0x42 0x4D
    while True:
        byte = ser.read(1)
        if byte == b'\x42':
            next_byte = ser.read(1)
            if next_byte == b'\x4d':
                frame = ser.read(30)  # remaining 30 bytes
                data = parse_pms5003_frame(frame)
                return data

def parse_pms5003_frame(frame):
    """Parse 30-byte PMS5003 data frame"""
    # Unpack: frame_length, pm1_std, pm25_std, pm10_std,
    #         pm1_env, pm25_env, pm10_env, ...
    values = struct.unpack('>HHHHHHHHHHHHHH', frame[:28])

    return {
        'pm1_standard':  values[1],   # µg/m³ standard conditions
        'pm25_standard': values[2],
        'pm10_standard': values[3],
        'pm1_env':       values[4],   # µg/m³ environmental conditions
        'pm25_env':      values[5],   # ← this is what apps typically display
        'pm10_env':      values[6],
        'particles_03um': values[7],  # count per 0.1L air
        'particles_05um': values[8],
        'particles_10um': values[9],
        'particles_25um': values[10],
    }

if __name__ == "__main__":
    while True:
        reading = read_pms5003()
        print(f"PM2.5: {reading['pm25_env']} µg/m³")
        print(f"PM10:  {reading['pm10_env']} µg/m³")
        print(f"Particles >0.3µm: {reading['particles_03um']} per 0.1L")
        print("---")
        time.sleep(5)

Enter fullscreen mode Exit fullscreen mode

Note: Install pyserial via pip install pyserial. Make sure your user is in the dialout group on Linux: sudo usermod -a -G dialout $USER

Converting Raw Readings to AQI

Raw µg/m³ values are hard for end users to interpret. You'll want to convert them to an Air Quality Index (AQI) score. Here's a Python implementation of the US EPA AQI breakpoints for PM2.5:

def pm25_to_aqi(pm25: float) -> dict:
    """
    Convert PM2.5 concentration (µg/m³) to AQI score.
    Based on US EPA 2024 breakpoints.
    Returns: {'aqi': int, 'category': str, 'color': str}
    """
    breakpoints = [
        # (AQI_low, AQI_high, PM25_low, PM25_high, category, color)
        (0,   50,  0.0,  9.0,   "Good",                "#00e400"),
        (51,  100, 9.1,  35.4,  "Moderate",             "#ffff00"),
        (101, 150, 35.5, 55.4,  "Unhealthy for Some",   "#ff7e00"),
        (151, 200, 55.5, 125.4, "Unhealthy",             "#ff0000"),
        (201, 300, 125.5, 225.4,"Very Unhealthy",        "#8f3f97"),
        (301, 500, 225.5, 500.4,"Hazardous",             "#7e0023"),
    ]

    for aqi_lo, aqi_hi, pm_lo, pm_hi, category, color in breakpoints:
        if pm_lo <= pm25 <= pm_hi:
            # Linear interpolation
            aqi = ((aqi_hi - aqi_lo) / (pm_hi - pm_lo)) * (pm25 - pm_lo) + aqi_lo
            return {
                'aqi': round(aqi),
                'category': category,
                'color': color,
                'pm25': pm25
            }

    return {'aqi': 500, 'category': 'Hazardous', 'color': '#7e0023', 'pm25': pm25}


# Example usage
result = pm25_to_aqi(18.3)
print(result)
# → {'aqi': 63, 'category': 'Moderate', 'color': '#ffff00', 'pm25': 18.3}

Enter fullscreen mode Exit fullscreen mode

Where HEPA-Equipped Devices Fit into Your Architecture

Here's where it gets interesting for app developers: modern commercial washroom equipment ships with built-in HEPA filtration. Automatic hand dryers like the Hyginest TurboDry Series include HEPA filters to prevent recirculating bathroom bacteria while drying hands.

In a smart building context, this means every hand dryer activation is a micro air-filtration event. If you're building an indoor air quality dashboard for offices, hospitals, or commercial spaces, these devices become passive air quality contributors.

A simple architecture for a smart washroom air quality monitor:


import paho.mqtt.client as mqtt
import json
import time

# Publish air quality readings to MQTT broker
def publish_air_quality(sensor_data: dict, location: str = "washroom-floor2"):
    client = mqtt.Client()
    client.connect("your-mqtt-broker.local", 1883, 60)

    aqi_data = pm25_to_aqi(sensor_data['pm25_env'])

    payload = {
        "location": location,
        "timestamp": time.time(),
        "pm25": sensor_data['pm25_env'],
        "pm10": sensor_data['pm10_env'],
        "aqi": aqi_data['aqi'],
        "category": aqi_data['category'],
        "hepa_device_nearby": True  # flag if HEPA hand dryer installed
    }

    client.publish(f"airquality/{location}", json.dumps(payload))
    client.disconnect()
    print(f"Published: AQI {aqi_data['aqi']} ({aqi_data['category']})")
Enter fullscreen mode Exit fullscreen mode

Install MQTT client: pip install paho-mqtt

Using the World Air Quality Index (WAQI) API

If you don't have hardware sensors, you can still build air quality apps using public APIs. The WAQI API gives you real-time AQI data for 100+ countries:

import requests

def get_city_aqi(city: str, api_token: str) -> dict:
    """
    Fetch real-time AQI data for a city using WAQI API.
    Get free token at: https://aqicn.org/data-platform/token/
    """
    url = f"https://api.waqi.info/feed/{city}/?token={api_token}"
    response = requests.get(url)
    data = response.json()

    if data['status'] == 'ok':
        iaqi = data['data']['iaqi']
        return {
            'city': city,
            'aqi': data['data']['aqi'],
            'pm25': iaqi.get('pm25', {}).get('v', 'N/A'),
            'pm10': iaqi.get('pm10', {}).get('v', 'N/A'),
            'temperature': iaqi.get('t', {}).get('v', 'N/A'),
            'humidity': iaqi.get('h', {}).get('v', 'N/A'),
            'dominant_pollutant': data['data'].get('dominentpol', 'N/A'),
            'station': data['data']['city']['name'],
        }
    return {'error': data.get('data', 'API error')}


# Example
token = "your-api-token-here"
ahmedabad_air = get_city_aqi("ahmedabad", token)
print(ahmedabad_air)
# → {'city': 'ahmedabad', 'aqi': 142, 'pm25': 58.3, ...}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways for Developers

  1. Understand the physics: HEPA's 99.97% efficiency at 0.3µm means it's even better at capturing the PM2.5 particles your sensors measure. A room with active HEPA filtration will show measurably lower readings.
  2. Use the right sensor metric: Always use pm25_env (environmental) over pm25_standard for real-world indoor monitoring. Standard values assume fixed atmospheric conditions.
  3. Account for HEPA devices in your data model: In commercial buildings, HEPA-equipped devices (air purifiers, certain hand dryers) act as continuous passive filters. Flag their presence in your location metadata.
  4. AQI ≠ PM2.5: Always convert µg/m³ to AQI for user-facing displays. Raw concentrations confuse non-technical users.
  5. Sensor placement matters: Place sensors at breathing height (1.2–1.5m) and away from direct airflow from HEPA devices - else your readings will look artificially clean directly in front of the device.

Top comments (0)