Created
July 21, 2026 10:18
-
-
Save cmj/3e322fcbb55f055f28ac7e782a629d9a to your computer and use it in GitHub Desktop.
Grab screenshot of PurpleAir's US EPA PM2.5 AQI map
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # headless playwright script that screenshots the map canvas from PurpleAir's US EPA PM2.5 AQI map | |
| # ex: add cronjob and use with KDE media frame widget https://userbase.kde.org/Plasma/PictureFrame | |
| # */30 * * * * /usr/local/bin/purpleair.py -q --latest --dir=~/purpleair | |
| import argparse | |
| import shutil | |
| import subprocess | |
| from datetime import datetime | |
| from pathlib import Path | |
| from playwright.sync_api import sync_playwright | |
| # map baselayer options: /l opt: '/ls' s=sat, d=dark, t=topo p=detailed (default) | |
| # CONUS | |
| #URL_TEMPLATE = "https://map.purpleair.com/air-quality-standards-us-epa-aqi?opt=/1/l{layer}/a10/p604800/cC0#3.74/39.61/-95.87" | |
| # PNW | |
| URL_TEMPLATE = "https://map.purpleair.com/air-quality-standards-us-epa-aqi?opt=/1/l{layer}/a10/p604800/cC0#5.54/46.311/-118.371" | |
| BASELAYERS = {"s": "satellite", "d": "dark", "t": "topo", "p": "detailed"} | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--headed", action="store_true", help="Open a browser window instead of headless") | |
| parser.add_argument("--view", action="store_true", help="Open the screenshot after saving (uses xdg-open unless --viewer is set)") | |
| parser.add_argument("--viewer", help="Command to open the screenshot with instead of xdg-open (implies --view)") | |
| parser.add_argument( | |
| "--baselayer", | |
| choices=BASELAYERS.keys(), | |
| default="s", | |
| help="Map baselayer: s=satellite (default), d=dark, t=topo, p=detailed", | |
| ) | |
| parser.add_argument( | |
| "--latest", | |
| action="store_true", | |
| help="Save a copy to purpleair-latest.png (single updated file for KDE media frame widget)", | |
| ) | |
| parser.add_argument("-q", "--quiet", action="store_true", help="Suppress print output") | |
| parser.add_argument( | |
| "--dir", "--savedir", | |
| dest="savedir", | |
| default=".", | |
| help="Directory to save screenshots to (default: current directory)", | |
| ) | |
| args = parser.parse_args() | |
| URL = URL_TEMPLATE.format(layer=args.baselayer) | |
| savedir = Path(args.savedir).expanduser() | |
| savedir.mkdir(parents=True, exist_ok=True) | |
| with sync_playwright() as p: | |
| browser = p.chromium.launch(headless=not args.headed) | |
| page = browser.new_page(viewport={"width": 1600, "height": 900}) | |
| page.goto(URL) | |
| canvas = page.locator(".mapboxgl-canvas") | |
| canvas.wait_for(state="visible") | |
| # long wait here, but needed for icons/markers to populate | |
| page.wait_for_timeout(7000) | |
| page.evaluate(""" | |
| document.querySelectorAll('.paControlContainer').forEach(el => el.remove()); | |
| const gdpr = document.getElementById('gdpr-cookie-message'); | |
| if (gdpr) gdpr.remove(); | |
| document.querySelectorAll('header').forEach(el => el.remove()); | |
| """) | |
| page.evaluate("window.dispatchEvent(new Event('resize'))") | |
| page.wait_for_timeout(1000) | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| filename = savedir / f"purpleair_map_{timestamp}.png" | |
| canvas.screenshot(path=str(filename)) | |
| if not args.quiet: | |
| print(f"Screenshot saved {filename}") | |
| if args.latest: | |
| latest_filename = savedir / "purpleair-latest.png" | |
| shutil.copyfile(filename, latest_filename) | |
| if not args.quiet: | |
| print(f"Screenshot saved {latest_filename}") | |
| if args.headed: | |
| input("Screenshot saved. Browser is open for inspection - press Enter to close...") | |
| browser.close() | |
| if args.view or args.viewer: | |
| opener = args.viewer.split() if args.viewer else ["xdg-open"] | |
| subprocess.Popen(opener + [str(filename)]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment