# [Awair] Air Quality Dashboard
[](https://badge.fury.io/py/awair)
[](https://opensource.org/licenses/MIT)
[][awair.runsascoded.com]
A Python CLI tool and automated data collection system for [Awair] air quality sensors. Provides real-time data fetching using [the Awair API][API], historical analysis, automated S3 storage via AWS Lambda, and a web dashboard for visualization.
[][awair.runsascoded.com]
## Features
- **Web Dashboard**: Real-time visualization at [awair.runsascoded.com]
- **CLI Interface**: Raw data fetching, analysis, and export from Awair sensors
- **Automated Collection**: AWS Lambda function that collects data every 5 minutes
- **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
## Installation
### From PyPI (Recommended)
```bash
# Basic installation
pip install awair
# With Lambda deployment support
pip install awair[lambda]
# Development installation
pip install awair[dev]
```
### From Source
```bash
git clone https://github.com/runsascoded/awair.git
cd awair
pip install -e .
```
## Configuration
### API Token
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`
### Device Configuration
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
### Data Storage Location
Configure default data file path via:
- Environment variable: `export AWAIR_DATA_PATH="s3://your-bucket/data.parquet"`
- Local file: `echo "s3://your-bucket/data.parquet" > .awair-data-path`
- User config: `echo "s3://your-bucket/data.parquet" > ~/.awair/data-path`
- Default: `s3://380nwk/awair.parquet`
## Usage
### Data Collection
```bash
# Fetch raw API data and save to configured data file
awair raw --from-dt 250710T10 --to-dt 250710T11
# Fetch raw API data and output as JSONL to stdout
awair raw --from-dt 250710T10 --to-dt 250710T11 -d /dev/null
# Fetch only new data since latest timestamp in storage
awair raw --recent-only
# Check your account info
awair self
# List your devices
awair devices
```
### Data Analysis
```bash
# Show data file summary
awair data-info
awair data-info -d s3://your-bucket/data.parquet
# Daily histogram of record counts
awair hist
awair hist --from-dt 250710 --to-dt 250712
# Find timing gaps in data
awair gaps -n 5 -m 300 # Top 5 gaps over 5 minutes
```
### AWS Lambda Deployment
```bash
# Deploy automated data collector
awair lambda deploy
# View CloudFormation template
awair lambda synth
# Monitor logs
awair lambda logs --follow
# Test locally
awair lambda test
```
## Data Format
Sensor 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 |
### Example Data
```json
{"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}
```
## Architecture
### Automated Data Collection
The system uses AWS Lambda for automated data collection:
- **Schedule**: Runs every 5 minutes via EventBridge
- **Storage**: Updates S3 Parquet file incrementally
- **Efficiency**: Only fetches data since last update
- **Reliability**: Uses `utz.s3.atomic_edit` for safe concurrent updates
### CLI Integration
The CLI seamlessly works with both local files and S3:
```python
# Both work the same way
storage = ParquetStorage('local-file.parquet')
storage = ParquetStorage('s3://bucket/file.parquet')
```
### Configurable Deployments
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
## Development
### Setup
```bash
pip install -e ".[dev]"
```
### Code Style
```bash
ruff check
ruff format
```
### Testing
```bash
pytest
```
## Lambda Deployment
Deploy AWS Lambda function for automated data collection:
```bash
# Deploy latest PyPI version (recommended)
awair lambda deploy
# Deploy specific PyPI version
awair lambda deploy -v 0.0.1
# Deploy from local source (development)
awair lambda deploy -v source
# Build package only (no deploy)
awair lambda deploy --dry-run
awair lambda deploy -v 0.0.1 --dry-run
```
**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
## AWS Infrastructure
The Lambda deployment creates:
- **Lambda Function**: `awair-data-updater`
- **EventBridge Rule**: 5-minute schedule
- **IAM Role**: S3 permissions for target bucket
- **CloudWatch Logs**: 2-week retention
- **Environment Variables**: `AWAIR_TOKEN`, `AWAIR_DATA_PATH`
### Required AWS Permissions
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)
## Date/Time Format
The CLI uses a compact date format for convenience:
- `250710` → July 10, 2025
- `250710T16` → July 10, 2025 at 4 PM
- `20250710T1630` → July 10, 2025 at 4:30 PM
## License
MIT License - see LICENSE file for details.
[Awair]: https://www.getawair.com/
[API]: https://docs.developer.getawair.com/
[awair.runsascoded.com]: https://awair.runsascoded.com
Raw data
{
"_id": null,
"home_page": null,
"name": "awair",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.9",
"maintainer_email": "Ryan Williams <ryan@runsascoded.com>",
"keywords": "awair, air-quality, iot, sensors, aws, lambda, data-collection",
"author": null,
"author_email": "Ryan Williams <ryan@runsascoded.com>",
"download_url": "https://files.pythonhosted.org/packages/ce/ac/fb4b9af7c4971192565c998b2376f2141b35b2a6722179036a1c4656a0a4/awair-0.0.4.tar.gz",
"platform": null,
"description": "# [Awair] Air Quality Dashboard\n\n[](https://badge.fury.io/py/awair)\n[](https://opensource.org/licenses/MIT)\n[][awair.runsascoded.com]\n\nA Python CLI tool and automated data collection system for [Awair] air quality sensors. Provides real-time data fetching using [the Awair API][API], historical analysis, automated S3 storage via AWS Lambda, and a web dashboard for visualization.\n\n[][awair.runsascoded.com]\n\n## Features\n\n- **Web Dashboard**: Real-time visualization at [awair.runsascoded.com]\n- **CLI Interface**: Raw data fetching, analysis, and export from Awair sensors\n- **Automated Collection**: AWS Lambda function that collects data every 5 minutes\n- **S3 Storage**: Efficient Parquet format with incremental updates\n- **Data Analysis**: Built-in tools for gaps analysis, histograms, and data summaries\n- **Flexible Storage**: Works with local files or S3 (configurable default paths)\n- **AWS Deployment**: One-command CDK deployment with automatic IAM permissions\n\n## Installation\n\n### From PyPI (Recommended)\n\n```bash\n# Basic installation\npip install awair\n\n# With Lambda deployment support\npip install awair[lambda]\n\n# Development installation\npip install awair[dev]\n```\n\n### From Source\n\n```bash\ngit clone https://github.com/runsascoded/awair.git\ncd awair\npip install -e .\n```\n\n## Configuration\n\n### API Token\nSet your Awair API token via:\n- Environment variable: `export AWAIR_TOKEN=\"your-token\"`\n- Local file: `echo \"your-token\" > .token`\n- User config: `echo \"your-token\" > ~/.awair/token`\n\n### Device Configuration\nConfigure your Awair device via:\n- Environment variables: `export AWAIR_DEVICE_TYPE=\"awair-element\" AWAIR_DEVICE_ID=\"12345\"`\n- Local file: `echo \"awair-element,12345\" > .awair-device`\n- User config: `echo \"awair-element,12345\" > ~/.awair/device`\n- **Auto-discovery**: If not configured, the CLI will automatically detect your device on first use\n\n### Data Storage Location\nConfigure default data file path via:\n- Environment variable: `export AWAIR_DATA_PATH=\"s3://your-bucket/data.parquet\"`\n- Local file: `echo \"s3://your-bucket/data.parquet\" > .awair-data-path`\n- User config: `echo \"s3://your-bucket/data.parquet\" > ~/.awair/data-path`\n- Default: `s3://380nwk/awair.parquet`\n\n## Usage\n\n### Data Collection\n\n```bash\n# Fetch raw API data and save to configured data file\nawair raw --from-dt 250710T10 --to-dt 250710T11\n\n# Fetch raw API data and output as JSONL to stdout\nawair raw --from-dt 250710T10 --to-dt 250710T11 -d /dev/null\n\n# Fetch only new data since latest timestamp in storage\nawair raw --recent-only\n\n# Check your account info\nawair self\n\n# List your devices\nawair devices\n```\n\n### Data Analysis\n\n```bash\n# Show data file summary\nawair data-info\nawair data-info -d s3://your-bucket/data.parquet\n\n# Daily histogram of record counts\nawair hist\nawair hist --from-dt 250710 --to-dt 250712\n\n# Find timing gaps in data\nawair gaps -n 5 -m 300 # Top 5 gaps over 5 minutes\n```\n\n### AWS Lambda Deployment\n\n```bash\n# Deploy automated data collector\nawair lambda deploy\n\n# View CloudFormation template\nawair lambda synth\n\n# Monitor logs\nawair lambda logs --follow\n\n# Test locally\nawair lambda test\n```\n\n## Data Format\n\nSensor data is stored in Parquet format with these fields:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `timestamp` | datetime | UTC timestamp |\n| `temp` | float | Temperature (\u00b0F) |\n| `co2` | int | CO2 (ppm) |\n| `pm10` | int | PM10 particles |\n| `pm25` | int | PM2.5 particles |\n| `humid` | float | Humidity (%) |\n| `voc` | int | Volatile Organic Compounds |\n\n### Example Data\n\n```json\n{\"timestamp\":\"2025-07-05T22:22:06.331Z\",\"temp\":73.36,\"co2\":563,\"pm10\":3,\"pm25\":2,\"humid\":52.31,\"voc\":96}\n{\"timestamp\":\"2025-07-05T22:21:06.063Z\",\"temp\":73.33,\"co2\":562,\"pm10\":3,\"pm25\":2,\"humid\":52.23,\"voc\":92}\n```\n\n## Architecture\n\n### Automated Data Collection\n\nThe system uses AWS Lambda for automated data collection:\n\n- **Schedule**: Runs every 5 minutes via EventBridge\n- **Storage**: Updates S3 Parquet file incrementally\n- **Efficiency**: Only fetches data since last update\n- **Reliability**: Uses `utz.s3.atomic_edit` for safe concurrent updates\n\n### CLI Integration\n\nThe CLI seamlessly works with both local files and S3:\n\n```python\n# Both work the same way\nstorage = ParquetStorage('local-file.parquet')\nstorage = ParquetStorage('s3://bucket/file.parquet')\n```\n\n### Configurable Deployments\n\nLambda deployments respect user configuration:\n\n- IAM permissions generated dynamically per S3 bucket\n- Environment variables passed to Lambda runtime\n- Support for any S3 bucket/key combination\n\n## Development\n\n### Setup\n\n```bash\npip install -e \".[dev]\"\n```\n\n### Code Style\n\n```bash\nruff check\nruff format\n```\n\n### Testing\n\n```bash\npytest\n```\n\n## Lambda Deployment\n\nDeploy AWS Lambda function for automated data collection:\n\n```bash\n# Deploy latest PyPI version (recommended)\nawair lambda deploy\n\n# Deploy specific PyPI version\nawair lambda deploy -v 0.0.1\n\n# Deploy from local source (development)\nawair lambda deploy -v source\n\n# Build package only (no deploy)\nawair lambda deploy --dry-run\nawair lambda deploy -v 0.0.1 --dry-run\n```\n\n**PyPI Deployment (Default):**\n- \u2705 **Exact Versions**: Deploy specific, tested releases\n- \u2705 **Immutable**: Consistent across environments\n- \u2705 **Traceable**: Clear version tracking in Lambda\n- \u2705 **Production Ready**: Uses published releases\n\n**Source Deployment (`-v source`):**\n- \ud83d\udd27 **Development**: Test local changes before publishing\n- \ud83d\ude80 **Latest Features**: Access unreleased functionality\n\n## AWS Infrastructure\n\nThe Lambda deployment creates:\n\n- **Lambda Function**: `awair-data-updater`\n- **EventBridge Rule**: 5-minute schedule\n- **IAM Role**: S3 permissions for target bucket\n- **CloudWatch Logs**: 2-week retention\n- **Environment Variables**: `AWAIR_TOKEN`, `AWAIR_DATA_PATH`\n\n### Required AWS Permissions\n\nFor deployment, you need permissions to create:\n- Lambda functions and layers\n- IAM roles and policies\n- EventBridge rules\n- CloudWatch log groups\n- S3 bucket access (for your target bucket)\n\n## Date/Time Format\n\nThe CLI uses a compact date format for convenience:\n\n- `250710` \u2192 July 10, 2025\n- `250710T16` \u2192 July 10, 2025 at 4 PM\n- `20250710T1630` \u2192 July 10, 2025 at 4:30 PM\n\n## License\n\nMIT License - see LICENSE file for details.\n\n[Awair]: https://www.getawair.com/\n[API]: https://docs.developer.getawair.com/\n[awair.runsascoded.com]: https://awair.runsascoded.com\n",
"bugtrack_url": null,
"license": null,
"summary": "Awair API client and data collection system with AWS Lambda automation",
"version": "0.0.4",
"project_urls": {
"Documentation": "https://github.com/runsascoded/awair#readme",
"Homepage": "https://github.com/runsascoded/awair",
"Issues": "https://github.com/runsascoded/awair/issues",
"Repository": "https://github.com/runsascoded/awair.git"
},
"split_keywords": [
"awair",
" air-quality",
" iot",
" sensors",
" aws",
" lambda",
" data-collection"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "fefeac516231e2f971e17df81c627af694b1322aac0528ac3542d0dc9663dccb",
"md5": "70e9d32b8ea01c3ec170cd1198d2f23a",
"sha256": "026a1d04461710707e46ec1d7f8eb75e2f534dd69e67b894e4794b1f3b6ced8e"
},
"downloads": -1,
"filename": "awair-0.0.4-py3-none-any.whl",
"has_sig": false,
"md5_digest": "70e9d32b8ea01c3ec170cd1198d2f23a",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.9",
"size": 27666,
"upload_time": "2025-07-15T14:48:02",
"upload_time_iso_8601": "2025-07-15T14:48:02.753501Z",
"url": "https://files.pythonhosted.org/packages/fe/fe/ac516231e2f971e17df81c627af694b1322aac0528ac3542d0dc9663dccb/awair-0.0.4-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "ceacfb4b9af7c4971192565c998b2376f2141b35b2a6722179036a1c4656a0a4",
"md5": "1ad2a75a4558cb1d6a40dcf9f1a22791",
"sha256": "e58aff5f1b6afc6792fea672ce63977af239dc527f55126911daf30705ed7bae"
},
"downloads": -1,
"filename": "awair-0.0.4.tar.gz",
"has_sig": false,
"md5_digest": "1ad2a75a4558cb1d6a40dcf9f1a22791",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.9",
"size": 27511,
"upload_time": "2025-07-15T14:48:03",
"upload_time_iso_8601": "2025-07-15T14:48:03.987987Z",
"url": "https://files.pythonhosted.org/packages/ce/ac/fb4b9af7c4971192565c998b2376f2141b35b2a6722179036a1c4656a0a4/awair-0.0.4.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-07-15 14:48:03",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "runsascoded",
"github_project": "awair#readme",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"requirements": [
{
"name": "click",
"specs": []
},
{
"name": "requests",
"specs": []
}
],
"lcname": "awair"
}