{"id":51136573,"url":"https://github.com/jasonjhofmann/aeolus","last_synced_at":"2026-06-25T18:30:52.162Z","repository":{"id":364183759,"uuid":"1260844419","full_name":"jasonjhofmann/aeolus","owner":"jasonjhofmann","description":"Aeolus — adaptive, multi-zone CO2 \u0026 ventilation manager for Home Assistant: shared-air-aware hysteresis control, safety vetoes, slope/ACH reporting. Platinum quality scale.","archived":false,"fork":false,"pushed_at":"2026-06-19T01:54:52.000Z","size":957,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-19T03:19:09.116Z","etag":null,"topics":["air-quality","co2","hacs","home-assistant","home-automation","homeassistant","integration","ventilation"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jasonjhofmann.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-05T23:58:33.000Z","updated_at":"2026-06-19T01:54:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jasonjhofmann/aeolus","commit_stats":null,"previous_names":["jasonjhofmann/aeolus"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jasonjhofmann/aeolus","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasonjhofmann%2Faeolus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasonjhofmann%2Faeolus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasonjhofmann%2Faeolus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasonjhofmann%2Faeolus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jasonjhofmann","download_url":"https://codeload.github.com/jasonjhofmann/aeolus/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasonjhofmann%2Faeolus/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34788252,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-25T02:00:05.521Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["air-quality","co2","hacs","home-assistant","home-automation","homeassistant","integration","ventilation"],"created_at":"2026-06-25T18:30:50.598Z","updated_at":"2026-06-25T18:30:52.154Z","avatar_url":"https://github.com/jasonjhofmann.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Aeolus\n\n**Adaptive, multi-zone CO₂ \u0026 ventilation manager for Home Assistant.**\n\n\u003e In Greek myth, **Aeolus** is the keeper of the winds — he holds many separate winds and releases each on demand. That is exactly what this integration does: it orchestrates multiple, cross-coupled air streams (ERV/HRV, exhausts, supply/transfer fans, windows) across rooms that *share air*, releasing the right one at the right time to manage CO₂ — while reporting the rate of change (slope) and the underlying air-change rate (ACH), in the spirit of [Versatile Thermostat](https://github.com/jmcollin78/versatile_thermostat).\n\n---\n\n## Status: ✅ Platinum quality scale\n\nThe spec is settled and the v0.1 MVP is implemented end-to-end: data model +\nverified config-subentry flows, the push engine (no coordinator), time-aware\nEMA + slope + gap-normalized ACH (unit-tested), per-space **hysteresis control**\nwith coverage arbitration over `direct` actuators, **safety vetoes** (stale\nsafe-state, filter-aware outdoor-AQ veto, per-actuator max-runtime), manual-\noverride yield, and all five entity platforms (sensor / binary_sensor / number /\nselect / switch). It loads on current Home Assistant with the integration test\nharness and the **full test suite passes** (EMA math, config + subentry flows,\nend-to-end setup, the control loop actuating a real entity, live dynamic add/\nremove of Spaces and Actuators, and repair-issue handling). Deferred to v1.1:\ninduced/pressure edges + escalation, variable drive, full CAZ + radon veto,\nauto-calibration.\n\n### Testing\n\n```bash\npython3 -m venv .venv \u0026\u0026 source .venv/bin/activate   # Python 3.13+\npip install -r requirements_test.txt\npython -m pytest                                     # from the repo root\n```\n\nCI (`.github/workflows/`) runs `ruff`, `mypy --strict`, and the test suite on a\n**Python 3.13 + 3.14** matrix (3.13 is the support floor), plus a Python-3.13\nsyntax-floor compile and hassfest + HACS validation, on every push and PR.\n\n**Quality scale: Platinum** — every Bronze + Silver + Gold + Platinum rule is complete: `mypy --strict` clean, fully async/non-blocking, and dependency-free local compute (`async-dependency`/`inject-websession` are N/A — no external library, no HTTP). The `brands` rule is satisfied by the in-package assets at `custom_components/aeolus/brand/` (icon/logo + dark variants), served locally via HA's Brands Proxy — `home-assistant/brands` no longer accepts custom-integration submissions, so the local folder is the supported path. **Next:** v1.1 polish (per-actuator influence-row config UI, variable-speed drive).\n\n- **[REQUIREMENTS.md](REQUIREMENTS.md)** — the full, versioned requirements specification (v3.1; §8 multi-pollutant built, §9 humidity planned).\n- **[docs/SCAFFOLD.md](docs/SCAFFOLD.md)** — repository structure, module responsibilities, build status, and the Quality-Scale roadmap.\n- **[CHANGELOG.md](CHANGELOG.md)** — spec history.\n- **`custom_components/aeolus/`** — the integration; **`tests/`** — pure-math unit tests.\n\n## What it will do (one paragraph)\n\nAeolus lets you select CO₂ sensors and ventilation actuators (fans, switches, ERV, windows) entirely through the UI, group them into **spaces**, and then keeps each space under a CO₂ target by activating actuators — explicitly modeling that **one actuator affects many rooms** and that **rooms share air** (direct, diffusive, and pressure-*induced* couplings). It reports a smoothed CO₂ value, a signed **slope** (ppm/min), and a concentration-normalized **effective air-change rate (ACH)** per space, and it refuses to trade one hazard for another (it won't ventilate into bad outdoor air, won't depressurize into a radon or combustion-safety problem, and won't let you pick a recirculating air purifier — which does nothing for CO₂).\n\n## Design targets\n\n- **Quality scale:** Home Assistant Integration **Quality Scale — Platinum**.\n- **Distribution:** HACS (custom integration).\n\n## Why this isn't \"just a CO₂ automation\"\n\nTwo correctness points drive the whole design (see REQUIREMENTS §1, §0.3):\n\n1. **CO₂ is removed only by air exchange**, never by filtration — HEPA/carbon/PCO/ionizers do nothing for CO₂. Aeolus enforces this at config time.\n2. **CO₂ decay is exponential toward an outdoor floor (~420 ppm)**, so the comparable effectiveness metric is the **air-change rate (ACH)**, not raw ppm/min (which scales with how high CO₂ currently is). Aeolus reports both.\n\n## Installation\n\n**HACS (recommended):** add `https://github.com/jasonjhofmann/aeolus` as a *custom repository* (type: Integration), install **Aeolus**, then restart Home Assistant. **Manual:** copy `custom_components/aeolus/` into your HA `config/custom_components/` and restart. Requires Home Assistant **2025.4 or newer** (config subentries).\n\nThen **Settings → Devices \u0026 Services → Add Integration → Aeolus** to create the manager.\n\n## Configuration\n\nAll configuration is in the UI. The single **Aeolus** entry holds two kinds of subentries:\n\n- **Space** — a managed zone. Pick its CO₂ sensor(s) and aggregation, a target and high threshold (ppm), optional volume, and an optional outdoor air-quality (PM) sensor + veto threshold.\n- **Actuator** — a ventilation device that reduces CO₂ (fan / switch / input_boolean / cover). Set its air *mechanism* (balanced ERV / supply / exhaust / transfer / window), the Spaces it directly serves, its intake filter efficiency (0–1), and an optional per-pathway intake AQ sensor.\n\nPer Space you get a CO₂ sensor (with slope + effective-ACH + time-to-target attributes), a mitigation/attention binary sensor, a target number, and a mode select (manage / monitor / off). A master **Management** switch pauses all control.\n\n## Actions\n\n- **`aeolus.recalibrate`** — clears observed/learned actuator effectiveness for an entry. Field: `config_entry_id`.\n\n## Supported devices\n\nAeolus is a **calculated** integration: it does not talk to any hardware directly. Instead it composes entities that already exist in your Home Assistant instance, so it works with **any** device whose data is exposed through a standard HA entity.\n\n**Sources it reads (per Space):**\n\n- **CO₂ sensors** — any `sensor` with `device_class: carbon_dioxide` (ppm). Tested with Aranet4, AirGradient, Airthings, SCD4x-based ESPHome sensors.\n- **Particulate / AQI sensors (optional, graduated ladders)** — any numeric `sensor` reporting PM1 / PM2.5 / PM10 (µg/m³), an AQI, or a generic value. Tested with AirVisual/IQAir, PurpleAir, AirGradient, Aranet Radon+.\n- **Outdoor air-quality sensor (optional veto)** — any PM `sensor` measuring the air a ventilation pathway would pull in.\n\n**Actuators it controls (per Actuator):**\n\n- `fan` (on/off and variable `percentage`), `switch`, `input_boolean`, and `cover` (windows/openers). One Actuator can drive a group of same-domain entities. Mechanisms: balanced (ERV/HRV), supply, exhaust, transfer, window, and filter (recirculating purifier — PM only, never CO₂).\n\nAeolus never modifies or reconfigures the entities it reads or controls; it only reads their state and calls their standard `turn_on` / `turn_off` / `set_percentage` / `open_cover` / `close_cover` services.\n\n## Supported functions\n\nPer **Space** (a device named after the zone), Aeolus creates:\n\n| Platform | Entity | Purpose |\n| --- | --- | --- |\n| `sensor` | **Managed `\u003cmetric\u003e`** (one per configured metric) | Time-smoothed (EMA) value of the zone's CO₂/PM/AQI. Carries `slope`, `effective_ach`, `time_to_target`, `status`, `reason`, and the live tier ladder as attributes. |\n| `sensor` | **Managed `\u003cmetric\u003e` slope** | Signed rate of change (per minute). *(diagnostic, disabled by default)* |\n| `sensor` | **Managed air change rate** (CO₂ only) | Concentration-normalized effective ACH (1/h). *(diagnostic)* |\n| `sensor` | **Status reason** | Plain-language explanation of why Aeolus is (or isn't) acting. *(diagnostic)* |\n| `binary_sensor` | **Mitigation active** | On while Aeolus is actively ventilating the zone. |\n| `binary_sensor` | **Attention** | On when a driven metric is over-high, not improving, or stale. *(diagnostic)* |\n| `number` | **Target CO₂** | Live setpoint the controller drives toward. |\n| `number` | **`\u003cmetric\u003e` threshold** | Tier-1 engage level for each non-CO₂ driven metric. |\n| `select` | **Mode** | `Manage` / `Monitor` / `Off` for the zone. *(config)* |\n| `switch` | **Manage `\u003cpollutant\u003e`** | Per-metric gate (only when a Space drives \u003e1 metric). *(config, disabled by default)* |\n\nPer **manager** (the single Aeolus device):\n\n| Platform | Entity | Purpose |\n| --- | --- | --- |\n| `switch` | **Management** | Master on/off for all Aeolus control. *(config)* |\n\n## How data updates\n\nAeolus is **push / event-driven — it does not poll**. It subscribes to `state_changed` and `state_reported` events for every configured source sensor and recomputes the affected Space's smoothed value, slope, and ACH the moment a reading arrives. A bounded **control tick runs every 60 s** to re-evaluate ventilation demand and refresh status/explainability even when no source has changed. Actuator state is likewise watched via `state_changed` so a manual/automation override is detected and yielded to immediately (or after a configurable confirmation delay for flappy cloud devices).\n\n## Use cases\n\n- **Single-room CO₂ control** — hold a bedroom or office below a target by cycling an exhaust fan, ERV, or window opener, with a deadband to prevent short-cycling.\n- **Multi-zone shared air** — one ERV serving several rooms; Aeolus models that one actuator affects many spaces and arbitrates demand across them.\n- **Don't trade one hazard for another** — an outdoor-AQ veto blocks pulling in dirty outdoor air; depressurizing mechanisms are penalized where they'd cause radon/combustion-safety problems; a recirculating purifier can never be selected for CO₂.\n- **Graduated particulate response** — drive a hood at 20 % when PM2.5 \u003e 30 and everything at 100 % when PM2.5 \u003e 80, ramping back down automatically as it clears.\n- **Self-auto-off loads** — re-arm a fan whose internal timer switches it off while its switch keeps reporting `on`.\n\n## Examples\n\nNotify when a zone needs attention:\n\n```yaml\nautomation:\n  - alias: \"Aeolus — Primary Bedroom needs attention\"\n    triggers:\n      - trigger: state\n        entity_id: binary_sensor.primary_bedroom_attention\n        to: \"on\"\n        for: \"00:10:00\"\n    actions:\n      - action: notify.mobile_app_phone\n        data:\n          message: \u003e-\n            {{ state_attr('binary_sensor.primary_bedroom_attention', 'reason') }}\n```\n\nPause all Aeolus control while guests are over, then resume:\n\n```yaml\nautomation:\n  - alias: \"Aeolus — pause during party scene\"\n    triggers:\n      - trigger: state\n        entity_id: input_boolean.party_mode\n    actions:\n      - action: switch.turn_{{ 'off' if trigger.to_state.state == 'on' else 'on' }}\n        target:\n          entity_id: switch.aeolus_management\n```\n\nChart the air-change rate a ventilation run is achieving:\n\n```yaml\ntype: history-graph\nentities:\n  - sensor.primary_bedroom_managed_air_change_rate\n  - sensor.primary_bedroom_managed_co2\n```\n\n## Known limitations\n\n- **Single manager instance.** One Aeolus config entry per Home Assistant; all Spaces and Actuators live under it as subentries.\n- **`Room volume (ft³)` is currently unused.** It is reserved for the planned occupancy/generation (equilibrium-CO₂) estimate. The air-change-rate readout is gap-normalized and does not need volume.\n- **`aeolus.recalibrate` is a stub.** Observed/learned-gain reset lands with the auto-calibration feature; the action is registered but does not yet clear gains.\n- **Induced/pressure edges + escalation are deferred to v1.1.** The current control loop arbitrates over `direct` actuators only; pressure-mediated (induced) coupling and direct→induced escalation, plus occupancy and radon/combustion (CAZ) vetoes, are planned.\n- **Continuous-run cap is a fixed 120 minutes.** An actuator running continuously for 120 minutes is force-stopped as a baseline safety cap (it then re-engages on the next cycle if still demanded). This cap is not yet user-configurable.\n- **Filtration removes particulates, never CO₂.** A `filter`-mechanism actuator is rejected for CO₂ duty by design.\n- **CO₂ is removed only by air exchange.** Aeolus reports ACH precisely because filtration does nothing for CO₂ — see *Why this isn't \"just a CO₂ automation\"* above.\n\n## Troubleshooting\n\n- **A fan/vent isn't turning on.** Check, in precedence order: the **Management** switch (master on/off) is on → the Space's **Mode** is `Manage` → for multi-metric Spaces, the relevant **Manage `\u003cpollutant\u003e`** switch is on. The **Status reason** sensor states the exact cause.\n- **Reason reads *\"Sensor unavailable — mitigation suspended\"* or *\"Sensor stale — mitigation suspended\"*.** The Space's CO₂ source(s) are unavailable, or haven't reported a usable value within the freshness window; Aeolus suspends control until they return. Confirm the source entity exists and is updating.\n- **Reason reads *\"manual override — yielding N min\"*.** Aeolus saw the actuator change to a state it didn't command (a person or another automation), so it yields for 30 minutes. For cloud devices (e.g. LG ThinQ) that briefly flap `unavailable → off → on`, set the actuator's **Manual-override confirmation delay** to ~2 minutes.\n- **Reason reads *\"outdoor-air quality veto\"*.** The configured outdoor PM sensor is above the veto threshold, so outdoor-air ventilation is blocked. Lower-risk options: assign a filtered intake (raise the actuator's filter efficiency) or relax the threshold.\n- **A configured sensor/actuator was deleted or renamed.** Aeolus raises a **repair issue** (\"Settings → Repairs\") naming the missing entity; reconfigure the affected Space/Actuator to point at the new entity.\n- **The fan turns off on its own.** If the load has an internal auto-off timer while its switch keeps reporting `on`, set the actuator's **Re-arm interval** so Aeolus re-sends the ON command periodically.\n\n## Removal\n\nDelete the **Aeolus** integration entry from Settings → Devices \u0026 Services (this removes its subentries, devices, and entities). For a manual install, also delete `custom_components/aeolus/` and restart. Aeolus never modifies the entities it reads or controls, so removing it simply stops the automatic ventilation control.\n\n## License\n\n[Apache-2.0](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjasonjhofmann%2Faeolus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjasonjhofmann%2Faeolus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjasonjhofmann%2Faeolus/lists"}