A low-cost Coefficient-Of-Performance meter for an air-to-water heat pump, built around an ESP8266 NodeMCU v2 with an SSD1306 OLED (Ideaspark "NodeMCU+OLED" board, roughly $7 assembled).
The meter measures hydronic-loop flow and cross-exchanger temperatures,
pulls the heat pump's electrical consumption from existing MQTT sources,
derives instantaneous and cumulative COP, and publishes everything under
the HPEM/ MQTT topic tree for Node-RED / InfluxDB / Grafana consumption.
More discussion about the project and the heat-pump system: https://www.reddit.com/r/DIYHeatPumps/comments/189mlyy/diy_airtowater_cop_measurement/
ESP_HeatMeter/— current working firmware. Also serves as a local snapshot of the code running (or staged to run) on the device.ESP_HeatMeter.ino— active sketchESP_HeatMeter (v2).not_ino— earlier experimental revision, kept alongside for referencearduino_secrets.h— not tracked (in.gitignore); contains Wi-Fi SSID/password and MQTT user/pass. Create your own from the#defineskeleton in the sketch.
sketch14_012424_sCOP/— prior generation sketch, left in place for history / diffing.old_setting_up_sketches/— incremental learning sketches from bring-up; ignored in.gitignore.setup_notes_8266-ssd1306_OLED.txt,pinout_setup_...odt— pin-out notes and wiring reference for the Ideaspark NodeMCU+OLED board.
- ESP8266 NodeMCU v2 + SSD1306 128×64 OLED (
u8g2library, I2C) - Hall-effect turbine flow meter on the hydronic loop (interrupt-driven pulse counter, calibrated in LPM)
- DS18B20 1-Wire temperature probes — up to ~12 channels, labelled for
process use:
EWT,LWT— entering / leaving water temps across the refrigerant heat exchanger (used for heat-output calculation)LIQ,VAP— refrigerant liquid / vapor line tempsRET_Bd,RET_HW,RET_Bsmt,RET_N,RET_S— zone return-water probes around the houseLAT,EAT— leaving / entering air temps on the duct coil
- OLED splash shows the firmware version string (currently
v041827)
heatQkW— instantaneous heat output = flow × ΔT(EWT − LWT) × ρ·cpheatEWh— running heat energy, integrated from heatQkWelecPkW— instantaneous electrical input (see "Dual power source" below)elecEWh— running electrical energy (mixed integration / metered-delta, see below)COP— instantaneous heatQkW / elecPkW, gated by a minimum flow threshold (minFlowLPM = 2.0) to suppress noise when the pump is offcCOP— cumulative COP over the current runsCOP— seasonal COP, accumulated across runsheat_sEQkWh,elec_sEkWh— seasonal heat and electrical energy totals
Published under HPEM/:
heatQkW heatEWh COP cCOP sCOP heat_sEQkWh elec_sEkWh
elecPkW elecEWh ePwrSrc flowLPM
EWT LWT VAP LIQ deltaT
RET_Bd RET_HW RET_Bsmt RET_N RET_S
LAT EAT
debug (per-iteration diagnostic trace — see below)
Subscribed:
gridvue/power/sensor/ashpgrid/state — grid-side ASHP watts (ESPHome gridvue)
emporia/ashp_og/power — off-grid ASHP watts (Emporia Vue via HA)
emporia/ashp_og/energy_today — off-grid cumulative kWh for the day
The heat pump can run either from the grid or from the off-grid battery/inverter system depending on a transfer switch. Two upstream monitors each see only their own side:
- Grid feed: an Emporia Vue-2 monitor flashed with ESPHome publishes
a ~5-second
gridvue/power/sensor/ashpgrid/statereading in watts. - Off-grid feed: a separate Emporia Vue (cloud-integrated via Home Assistant) exposes circuit-level power and daily-energy sensors, republished to MQTT by HA automations.
The sketch arbitrates per-message: whichever source reports the larger
power is treated as "active" (ePwrSource = 1 grid / 2 off-grid /
0 idle, threshold 5 W). Only one side carries current at a time in
practice, so the larger value is the true consumption.
- On-grid: the ESPHome feed updates at ~5 s cadence, so elecEWh is
integrated from
elecPkW × Δt. - Off-grid: HA's Emporia Vue integration publishes
energy_today(cumulative kWh) at a coarser cadence. elecEWh is advanced by the metered delta between consecutiveenergy_todaymessages, andelecPkWis derived fromΔenergy / Δtrather than integrated.
This hybrid approach avoids double-counting and keeps daily totals aligned with whichever meter actually saw the current.
sensor.ashp_og_energy_today is a total_increasing counter fed from
polled cloud data. The cloud occasionally reports an oversized
cumulative jump that self-corrects on the next poll — e.g. on
2026-04-18 at 14:08 EDT the counter stepped +205 Wh in 60 s while the
averaged power held at ~1960 W, producing a bogus 12.3 kW "spike" on
the derived power. The 20 kW sanity cap that previously guarded
derivedPkW was too loose to catch this.
The sketch now cross-checks derivedPkW against ogW (the averaged
power from emporia/ashp_og/power): when ogW > 100 W and
derivedPkW > 2 × ogW, the delta is declared a glitch, and
ogDeltaWh is reconstructed as ogW × Δt. Both the published
elecPkW and the elecEWh accumulator are fed the reconstructed
value, so neither the power plot nor the daily-energy total inherits
the artifact. The HPEM/debug topic emits dPkW, ogW, and a
GLITCH marker when the guard fires, so these events are visible
after the fact without needing serial capture.
HPEM/debug publishes a short trace on every electrical-side
iteration. Two formats:
- On-grid integration:
GR add=<Wh-added> PkW=<kW> dt=<ms> EWh=<total> - Off-grid consume:
OG dWh=<delta-Wh> EWh=<total> dtOg=<ms> dPkW=<derived kW> ogW=<avg-W>[ GLITCH]
These are intentionally noisy while the dual-power accounting is still being validated in the field. They'll be removed in a future flash once the numbers hold steady.
- IRAM headroom: the build sits at 93 % IRAM with "Basic SSL
ciphers" selected. Any new
IRAM_ATTR/ ISR code risks the link overflowing. Flash RAM and main RAM have plenty of room. - OTA is unreliable: at 93 % IRAM the Wi-Fi / SSL / OTA stacks coexist only marginally. Prefer USB flashing. The Arduino IDE's SSL-cipher dropdown does not persist between sessions, so double-check the "Basic SSL ciphers" selection before every compile.
Data flows into the household IOTstack (Mosquitto → Node-RED →
InfluxDB → Grafana). The "Heat Pump" and "Heat Pump cCOP sCOP"
Grafana dashboards plot these topics directly; the Garage Dash plots
HPEM_elecPkW as the ASHP_kw series alongside grid / off-grid
totals.