Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions docs/home_assistant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,135 @@ If you see "Entity not found" warnings:
1. Go to Settings -> Devices & Services -> Entities.
2. Search for "Aura" to see your actual entity IDs.
3. Open the dashboard code and use Find & Replace to swap the IDs with yours.

## Events Entity

Aura automatically registers a `sensor.<device_name>_events` entity via MQTT Discovery.
This sensor mirrors all significant runtime events to the HA logbook in real time —
matching what the built-in web dashboard **Events** tab already shows locally.

The sensor **state** is the human-readable event message, so it appears directly
in the HA logbook and history without any additional configuration.

### Attributes

| Attribute | Example value |
|---|---|
| `event_type` | `co2_bad`, `pm25_critical`, `system_warning`, `system_info` |
| `message` | `CO2 worsened to bad: 1480 ppm` |
| `category` | `SENSORS`, `TIME`, `RTC`, `STORAGE`, `FAN`, `WIFI`, `MQTT`, `PRESSURE`, `CHARTS`, `UI` |
| `value` | `1480.0` (air quality events only) |
| `unit` | `ppm` (air quality events only) |

### Air quality events

Fire on every band transition (good / moderate / bad / critical) for:
CO2, CO, PM0.5, PM1.0, PM2.5, PM4.0, PM10, HCHO, VOC, NOx.

### System events

#### Boot sequence

| Message | Category |
|---|---|
| `WiFi connected, IP: <ip>` | WIFI |
| `mDNS responder started: <hostname>.local` | WIFI |
| `MQTT connected` | MQTT |
| `NTP sync start (tz=<timezone>)` | TIME |
| `NTP sync completed, local time=<timestamp>` | TIME |
| `BMP580/581 OK` / `BMP3xx OK` / `DPS310 OK` | SENSORS |
| `SFA30 OK` | SENSORS |
| `SEN0466 CO OK at 0x<addr>` | SENSORS |
| `Pressure history restored` | PRESSURE |
| `Charts history restored` | CHARTS |
| `boot screens released` | UI |

#### Sensors

| Message | Category |
|---|---|
| `SEN66 not found (x/y)` | SENSORS |
| `SEN66 start attempts exhausted, stop probing until reboot` | SENSORS |
| `SEN66 OK` | SENSORS |
| `SFA30 init failed` | SENSORS |
| `Pressure sensor not found` | SENSORS |

#### Time

| Message | Category |
|---|---|
| `NTP sync timeout after <N> ms` | TIME |
| `RTC init failed` | RTC |
| `RTC read failed repeatedly` | RTC |
| `RTC communication restored` | RTC |
| `RTC battery low` | RTC |
| `RTC battery status OK` | RTC |

#### Storage

| Message | Category |
|---|---|
| `config committed as last known good` | STORAGE |
| `last known good commit failed` | STORAGE |
| `restored last good config` | STORAGE |
| `last good config missing, factory reset` | STORAGE |
| `factory reset requested` | STORAGE |
| `LittleFS mount failed` | STORAGE |

#### Fan / DAC

| Message | Category |
|---|---|
| `DAC ready` | FAN |
| `DAC recovered` | FAN |
| `DAC error: <reason>` | FAN |

#### Network

| Message | Category |
|---|---|
| `WiFi connect failed` | WIFI |
| `WiFi AP failed to start` | WIFI |
| `mDNS start failed` | WIFI |

#### History

| Message | Category |
|---|---|
| `Pressure history: 3h delta ready` | PRESSURE |
| `Pressure history: 24h delta ready` | PRESSURE |
| `Pressure history: filling gap <N>s` | PRESSURE |
| `Pressure history: stale, reset` | PRESSURE |
| `Pressure history: gap <N>s, reset` | PRESSURE |
| `Pressure history: invalid header, reset` | PRESSURE |
| `Pressure history: invalid index/count, reset` | PRESSURE |
| `Charts history: stale, reset` | CHARTS |
| `Charts history: epoch moved backwards, reset` | CHARTS |
| `Charts history: invalid header, reset` | CHARTS |
| `Charts history: invalid index/count, reset` | CHARTS |

### Example automation

```yaml
trigger:
- platform: state
entity_id: sensor.aura_events
condition:
- condition: template
value_template: >
{{ trigger.to_state.attributes.get('event_type') in
['co2_bad', 'co2_critical', 'pm25_bad', 'pm25_critical'] }}
action:
- service: notify.mobile_app
data:
message: "Aura: {{ trigger.to_state.state }}"
```

### Example logbook card

```yaml
type: logbook
entities:
- sensor.aura_<device_name>_events
hours_to_show: 24
```
22 changes: 22 additions & 0 deletions src/modules/ChartsHistory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
#include <string.h>
#include "core/Logger.h"
#include "modules/StorageManager.h"
#ifndef UNIT_TEST
// [NEW] Included for system event publishing to Home Assistant.
#include "modules/MqttManager.h"
#endif

namespace {

Expand Down Expand Up @@ -72,19 +76,28 @@ void ChartsHistory::load(StorageManager &storage) {

if (state_.magic != kChartsHistoryMagic || state_.version != kChartsHistoryVersion) {
LOGW("ChartsHistory", "invalid stored history header, reset");
#ifndef UNIT_TEST
MqttPublishSystemEvent("CHARTS", "warning", "Charts history: invalid header, reset");
#endif
reset(storage, true);
return;
}

if (state_.index >= kCapacity || state_.count > kCapacity) {
LOGW("ChartsHistory", "invalid stored index/count, reset");
#ifndef UNIT_TEST
MqttPublishSystemEvent("CHARTS", "warning", "Charts history: invalid index/count, reset");
#endif
reset(storage, true);
return;
}

uint32_t now_epoch = 0;
if (getNowEpoch(now_epoch) && isStale(now_epoch)) {
LOGW("ChartsHistory", "stored history stale, reset");
#ifndef UNIT_TEST
MqttPublishSystemEvent("CHARTS", "warning", "Charts history: stale, reset");
#endif
reset(storage, true);
return;
}
Expand All @@ -96,6 +109,9 @@ void ChartsHistory::load(StorageManager &storage) {
static_cast<unsigned>(state_.count),
static_cast<unsigned>(state_.index),
static_cast<unsigned>(state_.epoch));
#ifndef UNIT_TEST
MqttPublishSystemEvent("CHARTS", "info", "Charts history restored");
#endif
}

void ChartsHistory::saveIfDue(StorageManager &storage, uint32_t now_ms) {
Expand Down Expand Up @@ -226,6 +242,9 @@ void ChartsHistory::update(const SensorData &data, StorageManager &storage) {
bool time_valid = getNowEpoch(now_epoch);
if (time_valid && isStale(now_epoch)) {
LOGW("ChartsHistory", "history stale, reset");
#ifndef UNIT_TEST
MqttPublishSystemEvent("CHARTS", "warning", "Charts history: stale, reset");
#endif
reset(storage, true);
last_sample_ms_ = now_ms - step_ms;
}
Expand All @@ -235,6 +254,9 @@ void ChartsHistory::update(const SensorData &data, StorageManager &storage) {
if (time_valid && state_.epoch != 0) {
if (now_epoch < state_.epoch) {
LOGW("ChartsHistory", "epoch moved backwards, reset");
#ifndef UNIT_TEST
MqttPublishSystemEvent("CHARTS", "warning", "Charts history: epoch moved backwards, reset");
#endif
reset(storage, true);
last_sample_ms_ = now_ms - step_ms;
} else {
Expand Down
13 changes: 13 additions & 0 deletions src/modules/FanControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
#include "config/AppConfig.h"
#include "config/AppData.h"
#include "core/Logger.h"
// [NEW] Include MqttManager for system event publishing to Home Assistant.
// FanControl runs only on the ESP32 target, so no UNIT_TEST guard is needed.
#include "modules/MqttManager.h"

namespace {

Expand Down Expand Up @@ -252,6 +255,8 @@ void FanControl::begin(bool auto_mode_preference, bool auto_armed_preference) {
switch (tryInitialize(now_ms, init_failure_reason)) {
case InitStatus::Ok:
LOGI("FanControl", "DAC ready at 0x%02X", Config::DAC_I2C_ADDR_DEFAULT);
// [NEW] DAC hardware found and initialised — mirrors "DAC ready" in web dashboard Events tab.
MqttPublishSystemEvent("FAN", "info", "DAC ready");
break;
case InitStatus::Absent:
LOGI("FanControl", "DAC not installed");
Expand Down Expand Up @@ -314,6 +319,8 @@ void FanControl::poll(uint32_t now_ms, const SensorData *sensor_data, bool gas_w
const char *init_failure_reason = nullptr;
if (tryInitialize(now_ms, init_failure_reason) == InitStatus::Ok) {
LOGI("FanControl", "DAC recovered");
// [NEW] DAC recovered after a fault — mirrors "DAC recovered" in Events tab.
MqttPublishSystemEvent("FAN", "info", "DAC recovered");
}
}
} else if (!running_ &&
Expand Down Expand Up @@ -666,6 +673,12 @@ bool FanControl::applyOutputMillivolts(uint16_t millivolts) {

void FanControl::handleDacFault(const char *reason) {
LOGW("FanControl", "DAC error: %s", reason ? reason : "unknown");
// [NEW] DAC hardware fault — mirrors "DAC error: <reason>" in web dashboard Events tab.
{
char msg[64];
snprintf(msg, sizeof(msg), "DAC error: %s", reason ? reason : "unknown");
MqttPublishSystemEvent("FAN", "warning", msg);
}
available_ = false;
faulted_ = true;
applyStopState(false);
Expand Down
Loading