Awair Air Quality Dashboard
A Python CLI tool and automated data collection system for Awair air quality sensors. Provides real-time data fetching using the Awair API, historical analysis, automated S3 storage via AWS Lambda (per-device), and a web dashboard for visualization.
- Web Dashboard: Real-time visualization at awair.runsascoded.com
- CLI Interface: Raw data fetching, analysis, and export from Awair sensors
- Automated Collection: AWS Lambda functions that collect data every minute per device
- Multi-Device Support: Separate Lambda stacks and Parquet files per device
- S3 Storage: Efficient Parquet format with incremental updates
- Data Analysis: Built-in tools for gaps analysis, histograms, and data summaries
- Flexible Storage: Works with local files or S3 (configurable default paths)
- AWS Deployment: One-command CDK deployment with automatic IAM permissions
# Basic installation
pip install awair
# With Lambda deployment support
pip install awair[lambda]
# Development installation
pip install awair[dev]git clone https://github.com/runsascoded/awair.git
cd awair
pip install -e .Set your Awair API token via:
- Environment variable:
export AWAIR_TOKEN="your-token" - Local file:
echo "your-token" > .token - User config:
echo "your-token" > ~/.awair/token
API Rate Limits:
- Hobbyist tier (default): Advertised 500/day, observed up to 1,280/day
- Enterprise tier: 86,400/day (60 requests/minute)
- Contact
[email protected]to request a rate limit increase - Thanks to Steve at Awair for upgrading this project to Enterprise tier!
Configure your Awair device via:
- Environment variables:
export AWAIR_DEVICE_TYPE="awair-element" AWAIR_DEVICE_ID="12345" - Local file:
echo "awair-element,12345" > .awair-device - User config:
echo "awair-element,12345" > ~/.awair/device - Auto-discovery: If not configured, the CLI will automatically detect your device on first use
- Command-line flag: Use
-i/--device-idwith numeric ID or name pattern (regex):awair data info -i 17617(numeric ID)awair data info -i "Ryan"(name pattern)awair data info -i "^Awair 2$"(exact regex match)
Configure default data file path via:
- Environment variable:
export AWAIR_DATA_PATH="s3://your-bucket/awair-17617.parquet"(explicit path) - Local file:
echo "s3://your-bucket/awair-17617.parquet" > .awair-data-path - User config:
echo "s3://your-bucket/awair-17617.parquet" > ~/.awair/data-path - Path template:
export AWAIR_DATA_PATH_TEMPLATE="s3://your-bucket/awair-{device_id}.parquet"- Automatically interpolates
{device_id}from device configuration - Default template:
s3://380nwk/awair-{device_id}.parquet - Useful for multi-device setups where you switch between devices
- Automatically interpolates
# Fetch raw API data and save to configured data file
awair api raw --from-dt 250710T10 --to-dt 250710T11
# Fetch raw API data and output as JSONL to stdout
awair api raw --from-dt 250710T10 --to-dt 250710T11 -d /dev/null
# Fetch only new data since latest timestamp in storage
awair api raw --recent-only
# Check your account info
awair api self
# List your devices (cached, 1 hour TTL)
awair api devices
# Force refresh device list from API
awair api devices --refresh# Show data file summary
awair data info
awair data info -d s3://your-bucket/data.parquet
# Daily histogram of record counts
awair data hist
awair data hist --from-dt 250710 --to-dt 250712
# Find timing gaps in data
awair data gaps -n 5 -m 300 # Top 5 gaps over 5 minutes# Deploy automated data collector for a device (1-minute intervals)
awair lambda deploy -s awair-updater-17617 -r 1
# Deploy for multiple devices (separate stacks)
AWAIR_DEVICE_ID=17617 AWAIR_DATA_PATH=s3://bucket/awair-17617.parquet \
awair lambda deploy -s awair-updater-17617 -r 1
AWAIR_DEVICE_ID=137496 AWAIR_DATA_PATH=s3://bucket/awair-137496.parquet \
awair lambda deploy -s awair-updater-137496 -r 1
# View CloudFormation template
awair lambda synth
# Monitor logs (specify function name)
aws logs tail /aws/lambda/awair-updater-17617 --follow
# Test locally
awair lambda testSensor data is stored in Parquet format with these fields:
| Field | Type | Description |
|---|---|---|
timestamp |
datetime | UTC timestamp |
temp |
float | Temperature (°F) |
co2 |
int | CO2 (ppm) |
pm10 |
int | PM10 particles |
pm25 |
int | PM2.5 particles |
humid |
float | Humidity (%) |
voc |
int | Volatile Organic Compounds |
{"timestamp":"2025-07-05T22:22:06.331Z","temp":73.36,"co2":563,"pm10":3,"pm25":2,"humid":52.31,"voc":96}
{"timestamp":"2025-07-05T22:21:06.063Z","temp":73.33,"co2":562,"pm10":3,"pm25":2,"humid":52.23,"voc":92}The system uses AWS Lambda for automated data collection:
- Schedule: Runs every minute via EventBridge (configurable)
- Multi-Device: Separate Lambda stack per device
- Storage: Updates device-specific S3 Parquet file incrementally
- Efficiency: Only fetches data since last update
- Reliability: Uses
utz.s3.atomic_editfor safe concurrent updates - Scalability: Each device has its own schedule and S3 path
The CLI seamlessly works with both local files and S3:
# Both work the same way
storage = ParquetStorage('local-file.parquet')
storage = ParquetStorage('s3://bucket/file.parquet')Lambda deployments respect user configuration:
- IAM permissions generated dynamically per S3 bucket
- Environment variables passed to Lambda runtime
- Support for any S3 bucket/key combination
pip install -e ".[dev]"ruff check
ruff formatpytestDeploy AWS Lambda function for automated data collection:
# Deploy latest PyPI version for a device (recommended)
AWAIR_DATA_PATH=s3://bucket/awair-17617.parquet \
awair lambda deploy -s awair-updater-17617 -r 1
# Deploy specific PyPI version
awair lambda deploy -v 0.0.1 -s awair-updater-17617 -r 1
# Deploy from local source (development)
awair lambda deploy -v source -s awair-updater-17617 -r 1
# Build package only (no deploy)
awair lambda deploy --dry-runMulti-Device Deployment: Each device gets its own Lambda stack with independent:
- EventBridge schedule (default: 1 minute)
- S3 Parquet file (
awair-{device_id}.parquet) - CloudWatch logs
- IAM permissions
PyPI Deployment (Default):
- ✅ Exact Versions: Deploy specific, tested releases
- ✅ Immutable: Consistent across environments
- ✅ Traceable: Clear version tracking in Lambda
- ✅ Production Ready: Uses published releases
Source Deployment (-v source):
- 🔧 Development: Test local changes before publishing
- 🚀 Latest Features: Access unreleased functionality
Each Lambda deployment creates:
- Lambda Function:
awair-updater-{device_id}(e.g.,awair-updater-17617) - EventBridge Rule: Configurable schedule (default: 1 minute)
- IAM Role: S3 permissions for device-specific target path
- CloudWatch Logs: 2-week retention
- Environment Variables:
AWAIR_TOKEN,AWAIR_DATA_PATH,AWAIR_DEVICE_ID
Example: Two devices
Stack: awair-updater-17617
├─ Lambda: awair-updater-17617
├─ EventBridge: rate(1 minute)
└─ S3: s3://380nwk/awair-17617.parquet
Stack: awair-updater-137496
├─ Lambda: awair-updater-137496
├─ EventBridge: rate(1 minute)
└─ S3: s3://380nwk/awair-137496.parquet
For deployment, you need permissions to create:
- Lambda functions and layers
- IAM roles and policies
- EventBridge rules
- CloudWatch log groups
- S3 bucket access (for your target bucket)
The dashboard uses @rdub/og-lambda to generate dynamic Open Graph images for social media previews. A scheduled Lambda takes screenshots of the dashboard hourly and uploads to S3.
Setup:
cd www
pnpm add -D @rdub/og-lambdaConfiguration (.og-lambda.json):
{
"stackName": "awair-og",
"screenshotUrl": "https://awair.runsascoded.com/?og&t=-3d",
"s3Bucket": "380nwk",
"s3Key": "awair/og-image.jpg",
"scheduleRateMinutes": 60,
"waitForFunction": "window.chartReady",
"timezone": "America/New_York"
}Commands:
og-lambda status # Check Lambda status and schedule
og-lambda deploy # Deploy/update the Lambda
og-lambda invoke # Manually trigger a screenshot
og-lambda logs # Tail CloudWatch logsThe ?og URL parameter triggers a screenshot-optimized view (larger fonts, no controls).
The CLI uses a compact date format for convenience:
250710→ July 10, 2025250710T16→ July 10, 2025 at 4 PM20250710T1630→ July 10, 2025 at 4:30 PM
MIT License - see LICENSE file for details.