Back to Energy Monitoring
Home Intelligence — Phase 1 of 3

ERV Integration,
Monitoring & Weather Automation

Complete setup from bare hardware to a fully logged, weather-aware ERV control system. By end of Phase 1 both Broan units are on MQTT, Aqara indoor sensors are feeding the data pipeline, Open-Meteo forecasts drive automated ERV decisions, and everything is visible and controllable from a local web UI.
Broan B210E75RT ×2 ESP32-S3-DevKitC ×2 MAX485 Module ×2 MP1584EN Buck ×2 Aqara Hub V3 + Sensors Open-Meteo Forecast API Mosquitto MQTT SQLite + FastAPI
What Phase 1 Delivers
Outcomes by end of Phase 1
CapabilitySourceUsed For
ERV intake & exhaust temp, CFM, power, filter lifeRS-485 → ESP32 → MQTTLogging, efficiency calc, filter alerts
Per-room indoor temp + humidityAqara Hub V3 → MQTTComfort baseline, humidity triggers
Current outdoor temp + humidityOpen-Meteo (current hour)ERV decision — is outdoor air worth bringing in?
48-hour weather forecastOpen-Meteo (hourly)Pre-emptive ERV scheduling before weather changes
ERV fan mode + speed controlMQTT → ESP32 → RS-485Automated + manual override via web UI
Web dashboardFastAPI served locallyAll sensor data, charts, manual controls, automation status
SQLite historical logPython MQTT subscriberTrend analysis, efficiency over time, Phase 2 baseline
📋
Phase 1 scope boundaryPhase 1 does not include the Mitsubishi heat pump, Emporia Vue power monitoring, or heat pump automation. Those are Phase 2. Phase 1 focuses entirely on air quality, ventilation efficiency, and weather-driven ERV decisions.
Parts & Tools — Per ERV Unit
Hardware checklist (Amazon links + estimated costs)
Item Amazon Est. Cost Qty Est. Total Notes
Broan AI Series ERV (small unit) $1,075.58 1× $1,075.58 Comparable ERV70S pricing baseline
Aqara Hub V3 (M3) $129.99 1× $129.99 Hub V3 pricing aligned to M3 listing
Aqara Temp + Humidity Sensors $19.99 6× $119.94 6 sensors for room coverage
ESP32-S3-DevKitC-1-N16R8 $12.29 1× $12.29 One per ERV unit
MAX485 TTL-to-RS485 module $6.59 1× $6.59 Per ERV unit
MP1584EN buck converter (6-pack) $4.03 1× $4.03 $24.19 / 6-pack
24 AWG 2-conductor wire (33 ft) $7.22 shared $7.22 ~3 ft per ERV run
Female-to-female Dupont jumper wires (40-pin) $4.20 shared $4.20 Ribbon can be split into singles
USB-C data cable $9.88 shared $9.88 Initial ESP32 flash only
Multimeter $36.54 shared $36.54 Verify buck output at 5V
Soldering iron kit $11.98 shared $11.98 Attach wires to MP1584 pads
Total (per ERV unit) $1,098.49 Includes ERV unit + per-ERV electronics
Total (Aqara hub + 6 sensors) $249.93 Shared across all ERVs
Total (shared tools + consumables) $69.82 One-time tools used across all ERVs
Phase 1 Table of Contents
Jump to a section
Phase What to Expect
Phase 1A — Hardware Assembly Buck converter calibration, MAX485 wiring, RS-485 line checks.
Phase 1B — ESPHome Firmware Flash ESP32-S3, validate MQTT publishing, verify ERV sensor reads.
Phase 1C — MQTT Broker + Python Backend Mosquitto, SQLite logging, and the FastAPI dashboard backbone.
Phase 1D — Aqara Indoor Sensor Integration Bridge Aqara sensors into MQTT and confirm room-level data flow.
Phase 1E — Open-Meteo Weather Integration Forecast polling, outdoor conditions, and scheduler inputs.
Phase 1F — ERV Automation Rules Define the decision logic and validate automated ERV actions.
Draft Document
Multi-Controller for a Single ERV Single RS-485 master + multiple clients with logged command routing.
Phase 1A — Hardware Assembly
1

Set Buck Converter to 5V Before Touching the ESP32

MP1584 modules ship at an unknown output voltage. Connecting an unverified one to an ESP32 will destroy it instantly.
⚠️
Do this first, every time, for every modulePower the MP1584 from any 12V source (USB charger or the DROK set to 12V). Probe OUT+ and OUT− with a multimeter. Turn the brass pot screw slowly until you read 5.0–5.2V. Verify. Only then connect to the ESP32.
MP1584EN ADJ POT IN+ IN− OUT+ OUT− 12V GND 5V out ← measure here
2

Wire MAX485 Module to ESP32-S3

Dupont female-to-female jumpers only. No soldering for this step.
ESP32-S3 DevKitC-1 5V GND GPIO17 GPIO18 GPIO16 MAX485 VCC GND DI (TX) RO (RX) DE RE A (D−) B (D+) bridge → ERV D− → ERV D+
Connection table
ESP32-S3MAX485Notes
5V→VCCRed wire
GND→GNDBlack wire
GPIO17 (TX)→DIGreen wire
GPIO18 (RX)→ROBlue wire
GPIO16→DE + RE (bridged)Purple — tie DE and RE together first
3

Connect to ERV Terminal Block

Green 6-position terminal block near the ERV control area. You use 4 of the 6 terminals.
🔌
VTTOUCHW touchscreen will go silentThe ERV RS-485 bus only responds to one controller. Once the ESP32 is active, the touchscreen stops responding — this is a hardware limitation in the ERV firmware. The web dashboard replaces it entirely. The OVR dry-contact terminals still work for physical override if needed.
BROAN B210E75RT — TERMINAL BLOCK 12V +12V pwr → Buck IN+ GND Ground → Buck IN− D+ RS485+ → MAX485 B D− RS485− → MAX485 A LED unused OVR dry contact aux override
🔄
A/B labeling is reversed from D+/D−On MAX485 modules: A = D− and B = D+. If you see alignment errors in ESPHome logs, swap these two wires first.
⚡
Connect 12V terminal lastWire everything else first. The ERV 12V terminal is the final connection once all other wiring is verified.
Phase 1B — ESPHome Firmware
4

Flash ESPHome to ESP32-S3

One USB flash per unit. All future updates go over WiFi (OTA).
Install ESPHome + create secrets
# Python 3.9+ required
pip install esphome

# secrets.yaml — same folder as the ERV yaml, never commit to git
wifi_ssid: "YourSSID"
wifi_password: "YourWiFiPassword"
ota_password: "choose-a-password"
mqtt_user: "erv_monitor"
mqtt_password: "your-mqtt-password"
Key substitutions to set per unit in broan_erv_esphome.yaml
substitutions:
  device_name: broan-erv-1       # broan-erv-2 for second unit
  mqtt_prefix:  "erv/unit1"       # erv/unit2 for second unit

wifi:
  manual_ip:
    static_ip: 192.168.1.200     # .201 for second unit
    gateway:   192.168.1.1

mqtt:
  broker: 192.168.1.XXX          # your Mosquitto broker IP
Flash + verify
# Plug in via USB-C then:
esphome run broan_erv_esphome.yaml

# Watch for these in the logs:
[I] WiFi connected, IP: 192.168.1.200
[I] MQTT connected
[I] Broan ERV: received packet

# If you see alignment/unknown command errors → swap D+/D− wires
# Power-cycle the ERV (breaker) after first ESP32 connection
Phase 1C — MQTT Broker + Python Backend
5

Set Up Mosquitto MQTT Broker

Central message bus. All ESP32s publish here. Python backend subscribes here.
📦
Runs in a minimal LXCA Debian LXC with 256MB RAM is sufficient. No dedicated hardware required.
Install and configure
apt install mosquitto mosquitto-clients

# Create authenticated user:
mosquitto_passwd -c /etc/mosquitto/passwd erv_monitor

# /etc/mosquitto/conf.d/erv.conf
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd

systemctl restart mosquitto && systemctl enable mosquitto

# Verify data is flowing from ESP32:
mosquitto_sub -h localhost -u erv_monitor -P yourpassword -t "erv/#" -v
# Should see: erv/unit1/sensor/intake_temperature 68.4
6

Python Backend — ERV Logging + Web Dashboard

Subscribes to MQTT, writes to SQLite, serves the web UI and control API.
Install and start
mkdir -p /opt/erv_monitor && cd /opt/erv_monitor
pip install fastapi uvicorn paho-mqtt aiosqlite python-dotenv httpx

# Copy erv_monitor.py and .env.example, then:
cp .env.example .env
nano .env   # set MQTT_BROKER, credentials, DB_PATH

mkdir -p /var/lib/erv_monitor
python erv_monitor.py

# Dashboard available at:
http://your-server-ip:8080
systemd service — runs on boot
# /etc/systemd/system/erv-monitor.service
[Unit]
Description=ERV Monitor + Automation
After=network.target mosquitto.service

[Service]
WorkingDirectory=/opt/erv_monitor
ExecStart=/usr/bin/python3 erv_monitor.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

systemctl daemon-reload && systemctl enable --now erv-monitor
Phase 1D — Aqara Indoor Sensor Integration
A

Bridge Aqara Hub V3 Sensors to MQTT

You already have sensors placed throughout the house. This step pulls that data into the same MQTT pipeline as the ERV data so the automation engine can use room-by-room temperature and humidity.
🏠
Three paths — choose oneAll three end at the same place: your Aqara sensor readings on MQTT topics. Pick based on what you're comfortable running.
Path A — Home Assistant (recommended, most future-proof)

Install Home Assistant OS in its own LXC or VM. Add the official Aqara integration (Settings → Integrations → Aqara). Enable the MQTT integration in HA pointed at your Mosquitto broker. HA republishes every Aqara sensor automatically.

# HA MQTT topic pattern for Aqara sensors:
homeassistant/sensor/aqara_living_room_temp/state
homeassistant/sensor/aqara_bedroom_humidity/state
# Add these topic patterns to ERV_UNITS in erv_monitor.py
Path B — Aqara Cloud API direct poll (simplest, no HA needed)
# pip install python-aqara  (or use requests directly)
# Aqara developer.aqara.com — create app, get credentials

import httpx, asyncio

async def poll_aqara(mqtt_client):
    # POST to Aqara OpenAPI every 60 seconds
    headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
    r = await client.post(
        "https://open-usa.aqara.com/v3.0/open/api",
        json={"intent": "query.resource.value",
              "data": [{"subjectId": DEVICE_ID,
                         "resourceIds": ["4.1.85", "0.1.85"]}]}
              # 4.1.85 = temperature, 0.1.85 = humidity
    )
    for sensor in r.json()["result"]:
        mqtt_client.publish(
            f"aqara/{sensor['subjectId']}/{sensor['resourceId']}",
            sensor["value"]
        )
    await asyncio.sleep(60)
Path C — Zigbee2MQTT (most local, no cloud)
# Requires: SONOFF Zigbee 3.0 USB Dongle Plus (~$20)
# Plug into server. Install Zigbee2MQTT alongside Mosquitto.
# Aqara sensors re-pair to Z2M directly.
# WARNING: test one sensor first — pairing behavior varies.

# Z2M publishes natively to your broker:
zigbee2mqtt/living_room_sensor
# → {"temperature":68.4,"humidity":42,"battery":87}
Sensor placement for ERV automation purposes
LocationWhat it tells the automation engine
Main living floor — centralPrimary comfort reference and humidity trigger source
Upstairs hallway / landingHeat stratification — how much warmth has risen
Basement / mechanical roomWhole-home thermal mass baseline — slow to change
Near ERV supply-air outletTemperature of air the ERV is delivering — is it helping?
Phase 1E — Open-Meteo Weather Integration
B

Open-Meteo Forecast Polling

Free, no API key, no rate limit concerns at 15-minute polling intervals. Returns current conditions and 48-hour hourly forecast — exactly what ERV decisions need.
🌤️
Why forecast matters more than current conditionsThe ERV takes time to have an effect on indoor conditions. A decision made on current outdoor temp alone is reactive. A decision made knowing outdoor temp will drop 25°F in 4 hours is proactive — you ventilate aggressively now and close down before the cold front hits.
Add to erv_monitor.py — weather polling service
import httpx

# Eastern PA coordinates — replace with yours
FORECAST_URL = (
    "https://api.open-meteo.com/v1/forecast"
    "?latitude=40.XX&longitude=-75.XX"
    "&hourly=temperature_2m,relativehumidity_2m,"
    "dewpoint_2m,precipitation_probability,windspeed_10m"
    "&forecast_days=2"
    "&temperature_unit=fahrenheit&windspeed_unit=mph"
    "&timezone=America/New_York"
)

async def fetch_forecast() -> dict:
    async with httpx.AsyncClient() as client:
        r = await client.get(FORECAST_URL, timeout=10)
        h = r.json()["hourly"]
        return {
            "outdoor_temp_f":    h["temperature_2m"][0],
            "outdoor_humidity":  h["relativehumidity_2m"][0],
            "dewpoint_f":        h["dewpoint_2m"][0],
            "precip_prob_now":   h["precipitation_probability"][0],
            "temp_in_2h":        h["temperature_2m"][2],
            "temp_in_4h":        h["temperature_2m"][4],
            "temp_in_6h":        h["temperature_2m"][6],
            "temp_in_12h":       h["temperature_2m"][12],
            "temp_in_24h":       h["temperature_2m"][24],
            "humidity_in_4h":    h["relativehumidity_2m"][4],
            "temp_min_12h":      min(h["temperature_2m"][:12]),
            "temp_max_12h":      max(h["temperature_2m"][:12]),
        }

async def weather_poll_loop():
    while True:
        weather = await fetch_forecast()
        # Store in db, publish to MQTT, feed into ERV decision engine
        await record_weather(weather)
        await erv_decision_engine(weather)
        await asyncio.sleep(900)  # poll every 15 minutes
Phase 1F — ERV Automation Rules
C

Weather-Aware ERV Decision Engine

Runs every 5 minutes. Combines outdoor conditions, indoor sensor data, and forecast to decide ERV fan mode. Always logs the reason for every decision.
PriorityConditionActionReason
1 — Safety outdoor_temp < 10°F Min Extreme cold — prevent heat loss and frosting
2 — Humidity indoor_rh > 55% Max Indoor humidity too high regardless of outdoor conditions
3 — Forecast temp_in_4h < outdoor_temp − 20°F Max → then Min Cold front incoming — ventilate while cheap, close down before it hits
4 — Moisture outdoor_rh > indoor_rh + 20% Min Outdoor air much more humid — bringing it in worsens indoor conditions
5 — Ideal 45°F < outdoor_temp < 75°F AND outdoor_rh < 65% Max Best possible outdoor air — ventilate aggressively, energy cost near zero
6 — Cold 10°F < outdoor_temp < 45°F Med Maintain air quality without excessive heat exchange load
7 — Default all other conditions Med Baseline ventilation
Decision engine implementation
async def erv_decision_engine(weather: dict):
    indoor_rh   = await get_avg_indoor_humidity()   # from Aqara
    outdoor_f   = weather["outdoor_temp_f"]
    outdoor_rh  = weather["outdoor_humidity"]
    temp_in_4h  = weather["temp_in_4h"]

    # Evaluate rules in priority order
    if outdoor_f < 10:
        mode, reason = "Min", "extreme cold"
    elif indoor_rh > 55:
        mode, reason = "Max", "high indoor humidity"
    elif temp_in_4h < outdoor_f - 20:
        mode, reason = "Max", "cold front in 4h — pre-ventilate"
        # Schedule a switch to Min in 3.5 hours
        asyncio.create_task(delayed_mode_set("Min", delay_minutes=210))
    elif outdoor_rh > indoor_rh + 20:
        mode, reason = "Min", "outdoor more humid than indoor"
    elif 45 < outdoor_f < 75 and outdoor_rh < 65:
        mode, reason = "Max", "ideal outdoor conditions"
    elif outdoor_f < 45:
        mode, reason = "Med", "cold — balanced ventilation"
    else:
        mode, reason = "Med", "default"

    for unit_id in ERV_UNITS:
        await set_erv_mode(unit_id, mode, reason)
Phase 1G — Web UI
D

Dashboard — What It Shows and Controls

Served by the FastAPI backend at port 8080. No external access required — local network only.
Dashboard sections
SectionContent
ERV StatusPer-unit: current mode, fan speed, intake temp, exhaust temp, CFM, power draw, filter life remaining
Indoor ConditionsPer-room temp and humidity from Aqara sensors. Highlights any room above 55% RH or below comfort range.
Outdoor / WeatherCurrent outdoor temp + humidity (Open-Meteo), 12-hour forecast sparkline, next weather event flag
Automation StatusCurrent active rule, reason for current ERV mode, last 10 decisions with timestamps
Manual ControlsOverride fan mode per unit (Standby / Min / Med / Max / Turbo). Override expires after 2 hours, automation resumes.
Charts24-hour history for indoor temps, humidity, ERV power draw, and outdoor temp overlaid
AlertsFilter life < 14 days, humidity spike, ERV offline, sensor dropout
API endpoints — accessible at http://server:8080
EndpointMethodDescription
GET /GETWeb dashboard HTML
/api/state/{unit_id}GETCurrent snapshot all metrics
/api/weatherGETCurrent conditions + 12h forecast
/api/indoorGETAll Aqara sensor current values
/api/command/{unit}/fan_modePOSTSet ERV mode — triggers manual override flag
/api/automation/pausePOSTPause automation for N minutes
/api/history/{unit}/{metric}GETTime series data for charts
Troubleshooting Reference
Common problems and fixes
SymptomCauseFix
Alignment errors / unknown command spamD+/D− reversedSwap the two RS-485 wires at the ERV terminal block
ESP connects to WiFi but no ERV dataERV not restarted after connectPower-cycle the ERV (flip its breaker)
No MQTT messages at brokerWrong broker IP or credentialsVerify with mosquitto_sub test, check secrets.yaml
Aqara sensors not appearing in MQTTBridge not configuredConfirm HA MQTT integration is enabled and pointing at your broker
Open-Meteo returning wrong tempsWrong coordinatesVerify lat/lon in FORECAST_URL — use decimal degrees
Buck converter warm / ESP resetsOutput voltage too highRe-measure output with multimeter, readjust to 5.0–5.2V
📌
Phase 1 complete — what comes nextOnce both ERV units are logging, Aqara sensors are feeding indoor conditions, Open-Meteo is driving automated decisions, and the web UI is live — you have a fully operational air quality management system. Phase 2 adds the Mitsubishi Hyper Heat and Emporia Vue for temperature and energy cost intelligence.