Skip to content

Instantly share code, notes, and snippets.

@lucharo
Last active December 9, 2025 12:04
Show Gist options
  • Select an option

  • Save lucharo/61f3d56e221951a2b9aedf1c65429234 to your computer and use it in GitHub Desktop.

Select an option

Save lucharo/61f3d56e221951a2b9aedf1c65429234 to your computer and use it in GitHub Desktop.
Awair air quality monitor for macOS menu bar (SwiftBar plugin)

Awair Menu Bar Monitor

SwiftBar plugin to display Awair air quality data in your macOS menu bar with COโ‚‚ alerts.

๐Ÿ’ก Why this exists: High COโ‚‚ levels significantly impair cognitive function.

Features

  • ๐Ÿƒ๐Ÿ‘€๐Ÿ˜ฎโ€๐Ÿ’จ๐ŸชŸ๐Ÿšจ Evidence-based thresholds (colorblind-friendly icons)
  • โฌ†๏ธโ†—๏ธโžก๏ธโ†˜๏ธโฌ‡๏ธ Live COโ‚‚ trend tracking with 5 levels (1m/2m/5m deltas)
  • macOS notifications when crossing thresholds
  • Dropdown with full metrics (temp, humidity, VOC, PM2.5, trends)
  • Hides automatically when not on home network
  • Updates every 10 seconds for near real-time monitoring

Install

brew install swiftbar jq
mkdir -p ~/.config/swiftbar
ln -s /Users/lc140159/.scripts/awair/awair.10s.sh ~/.config/swiftbar/awair.10s.sh

Setup

  1. Configure SwiftBar:

    • Open SwiftBar app (from Applications)
    • Click the SwiftBar menu bar icon โ†’ Preferences
    • Set plugin folder to: ~/.config/swiftbar/
    • Click Refresh All
  2. Enable Awair Local API:

    • Open Awair Home app
    • Go to: Device Settings โ†’ Developer Option โ†’ Enable Local Sensors
  3. Configure IP (if needed):

    • Find your Awair's IP address in your router's admin interface (e.g., Linksys app, or web admin at 192.168.1.1)
    • Edit AWAIR_IP in awair.10s.sh if your device has a different IP than 192.168.1.74
    • Reserve/bind the IP to the Awair's MAC address in your router's DHCP settings so it doesn't change
    • Note: Router configuration varies by manufacturer and ISP provider

How It Works

Polling Interval: The .10s. in the filename tells SwiftBar to run the script every 10 seconds. You can change this by renaming the file:

  • .10s.sh = every 10 seconds (current)
  • .30s.sh = every 30 seconds
  • .1m.sh = every 1 minute
  • .5m.sh = every 5 minutes

Permissions: The symlink preserves executable permissions from the source file, so no chmod needed after setup.

Trend Tracking

The menu bar arrow is based on the 1-minute trend:

  • โฌ†๏ธ Very fast rise: >+50 ppm
  • โ†—๏ธ Rising: +25 to +50 ppm
  • โžก๏ธ Stable: -25 to +25 ppm
  • โ†˜๏ธ Falling: -50 to -25 ppm
  • โฌ‡๏ธ Very fast fall: <-50 ppm

The dropdown shows three trend windows:

  • Trend (1m): Immediate feedback (did opening the window help?)
  • Trend (2m): Short-term pattern
  • Trend (5m): Longer pattern (is ventilation consistently improving things?)

History is stored in /tmp/awair_co2_history (last 5 minutes, auto-cleaned).

Thresholds

Based on Allen et al. 2016 (Harvard) and Satish et al. 2012 (Berkeley).

Icon COโ‚‚ Cognitive Impact
๐Ÿƒ < 800 ppm Minimal
๐Ÿ‘€ 800+ ppm Subtle effects possible
๐Ÿ˜ฎโ€๐Ÿ’จ 1000+ ppm Affects complex thinking
๐ŸชŸ 1200+ ppm Significant impact
๐Ÿšจ 1500+ ppm Headaches, fatigue
#!/bin/bash
# <xbar.title>Awair Air Quality Monitor</xbar.title>
# <xbar.version>v1.0</xbar.version>
# <xbar.author>lucharo</xbar.author>
# <xbar.desc>Shows Awair air quality data with CO2 alerts</xbar.desc>
# <xbar.dependencies>curl,jq</xbar.dependencies>
# <swiftbar.hideRunInTerminal>true</swiftbar.hideRunInTerminal>
# <swiftbar.hideLastUpdated>true</swiftbar.hideLastUpdated>
# ============================================
# CONFIGURATION
# ============================================
AWAIR_IP="192.168.1.74"
# Thresholds (ppm)
# Based on research showing cognitive effects:
# - Allen et al. 2016 (Harvard): significant decline in decision-making at 950+ ppm
# - Satish et al. 2012 (Berkeley): measurable cognitive impact starting ~1000 ppm
# - ASHRAE recommends indoor CO2 < 1000 ppm above outdoor (~400), so ~1400 max
# Ref: https://ehp.niehs.nih.gov/doi/10.1289/ehp.1510037
CO2_MODERATE=800 # Subtle cognitive effects may begin
CO2_HIGH=1000 # Noticeable impact on complex thinking
CO2_POOR=1200 # Significant impairment
CO2_CRITICAL=1500 # Headaches, fatigue, poor decision-making
# State file for notification tracking
STATE_FILE="/tmp/awair_notification_state"
# ============================================
# Fetch data from Awair Local API
# ============================================
DATA=$(curl -s --connect-timeout 5 "http://${AWAIR_IP}/air-data/latest" 2>/dev/null)
# If not reachable (not home), silently hide from menu bar
if [ -z "$DATA" ] || [ "$DATA" = "null" ]; then
exit 0
fi
# Parse values using jq
CO2=$(echo "$DATA" | jq -r '.co2 // empty')
TEMP=$(echo "$DATA" | jq -r '.temp // empty')
HUMID=$(echo "$DATA" | jq -r '.humid // empty')
VOC=$(echo "$DATA" | jq -r '.voc // empty')
PM25=$(echo "$DATA" | jq -r '.pm25 // empty')
SCORE=$(echo "$DATA" | jq -r '.score // empty')
if [ -z "$CO2" ]; then
echo "โš ๏ธ Awair"
echo "---"
echo "Failed to parse Awair data"
exit 0
fi
# ============================================
# Determine color based on CO2 level
# ============================================
if [ "$CO2" -ge "$CO2_CRITICAL" ]; then
COLOR="red"
ICON="๐Ÿšจ"
STATUS="CRITICAL"
TIP="Significant impairment likely. Ventilate."
elif [ "$CO2" -ge "$CO2_POOR" ]; then
COLOR="orange"
ICON="๐ŸชŸ"
STATUS="POOR"
TIP="Cognitive impact. Open a window when possible."
elif [ "$CO2" -ge "$CO2_HIGH" ]; then
COLOR="yellow"
ICON="๐Ÿ˜ฎโ€๐Ÿ’จ"
STATUS="HIGH"
TIP="May affect complex thinking."
elif [ "$CO2" -ge "$CO2_MODERATE" ]; then
COLOR="white"
ICON="๐Ÿ‘€"
STATUS="MODERATE"
TIP="Subtle effects possible."
else
COLOR="green"
ICON="๐Ÿƒ"
STATUS="GOOD"
TIP=""
fi
# ============================================
# Send notification if threshold crossed
# ============================================
send_notification() {
osascript -e "display notification \"$2\" with title \"$1\" sound name \"Glass\""
}
LAST_STATE=$(cat "$STATE_FILE" 2>/dev/null || echo "GOOD")
if [ "$STATUS" = "CRITICAL" ] && [ "$LAST_STATE" != "CRITICAL" ]; then
send_notification "๐Ÿšจ COโ‚‚ Critical: ${CO2} ppm" "Significant impairment. Ventilate!"
echo "CRITICAL" > "$STATE_FILE"
elif [ "$STATUS" = "POOR" ] && [ "$LAST_STATE" != "CRITICAL" ] && [ "$LAST_STATE" != "POOR" ]; then
send_notification "๐ŸชŸ COโ‚‚ Poor: ${CO2} ppm" "Cognitive impact likely."
echo "POOR" > "$STATE_FILE"
elif [ "$STATUS" = "HIGH" ] && { [ "$LAST_STATE" = "GOOD" ] || [ "$LAST_STATE" = "MODERATE" ]; }; then
send_notification "๐Ÿ˜ฎโ€๐Ÿ’จ COโ‚‚ High: ${CO2} ppm" "May affect complex thinking."
echo "HIGH" > "$STATE_FILE"
elif [ "$STATUS" = "MODERATE" ] || [ "$STATUS" = "GOOD" ]; then
echo "$STATUS" > "$STATE_FILE"
fi
# ============================================
# Menu bar output
# ============================================
echo "${ICON} ${CO2} ppm | color=${COLOR}"
echo "---"
echo "Air Quality Score: ${SCORE}/100"
[ -n "$TIP" ] && echo "๐Ÿ’ก $TIP | color=${COLOR} size=12"
echo "---"
echo "COโ‚‚: ${CO2} ppm | color=${COLOR}"
echo "Temperature: ${TEMP}ยฐC"
echo "Humidity: ${HUMID}%"
[ -n "$VOC" ] && [ "$VOC" != "null" ] && echo "VOC: ${VOC} ppb"
[ -n "$PM25" ] && [ "$PM25" != "null" ] && echo "PM2.5: ${PM25} ยตg/mยณ"
echo "---"
echo "Thresholds"
echo "-- ๐Ÿƒ Good: < ${CO2_MODERATE} ppm"
echo "-- ๐Ÿ‘€ Moderate: ${CO2_MODERATE} ppm"
echo "-- ๐Ÿ˜ฎโ€๐Ÿ’จ High: ${CO2_HIGH} ppm"
echo "-- ๐ŸชŸ Poor: ${CO2_POOR} ppm"
echo "-- ๐Ÿšจ Critical: ${CO2_CRITICAL} ppm"
echo "---"
echo "Refresh | refresh=true"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment