Inspiration

Sleep trackers tell you what happened — a low HRV, a short deep sleep — and never why, because they cannot see the bedroom. A thermometer sees the room but not you. Neither one alone can answer the only question I cared about: when my room changed, what did my body do in the same minutes?

What it does

An ESP32 on the bedside table logs CO₂, temperature, humidity, noise and light every 2 minutes; a reed switch on the window records open/closed — a physical ground truth.

Every night Oura also keeps three 5-minute series: heart rate, HRV and sleep stage. Most apps only show their averages. Room Intelligence pulls them and joins every 5-minute point of my body with the room readings from the same minutes. One timeline: me and the room.

Every morning a Telegram report leads with that link:

  • YOU × YOUR ROOM — what the room did, what my HRV and pulse did, compared within the same sleep stage.
  • YOUR MAP — my HRV and pulse at each room temperature, across all nights, growing every night.
  • One thing to try tonight, and a night chart with my HRV drawn over the room.

What the first 7 nights with both data streams showed:

Room My HRV My pulse
20–21 °C (6 nights) 43 ms 60 bpm
22 °C (6 nights) 40 ms 60 bpm
23 °C (4 nights) 34 ms 66 bpm
24 °C (1 night) 29 ms 72 bpm
  • On the hottest night (23.1 °C) my HRV was 32 ms vs 42 ms on my other nights — lower in every sleep stage — and my pulse 69 vs 60 bpm.
  • On another night the room crossed 22.5 °C for 80 minutes before dawn: HRV 33 vs 39 ms in the same sleep stages the rest of that night.
  • Within a night, small swings of temperature showed no link to my HRV, also with a lag of up to an hour (a separate check on the same data). The room matters as a whole-night setting — so the fix is setting it before bed, not reacting at night.

It is an n-of-1 experiment on one person, not a medical device. Seven nights is a hint, not proof — and the report says exactly that: "warm minutes went with a worse HRV or pulse on 2 of 3 nights that had them. A hint, not a rule yet."

The v3 morning report for a real night, 15→16 Oct Built by Code_v3.gs from the real night of 15→16 Oct and drawn in Telegram's style; the text is the code's draft before Nemotron rewrites it.

How I built it

Principle: code calculates, the model explains, the reed switch verifies.

  • Hardware: ESP32 (Freenove WROVER) with an SCD41 CO₂ sensor, DHT22, a KY-038 microphone, the camera used only as a light meter (no image leaves the board) and a reed switch. The CO₂ sensor recalibrates itself after the window has been open long enough.
  • Ring × room fusion (Google Apps Script): reads Oura API v2 (OAuth2), glues broken nights back together, takes every 5-minute ring point and averages the room rows within ±3 minutes. It compares within the same sleep stage, because HRV is naturally different in deep, light and REM sleep: a warm stretch against the rest of the same night, and a night against my other nights, stage by stage. The rules are fixed in code: ±10 % for HRV, ±2 bpm for pulse, at least 30 minutes on each side. History lives in a Fusion 5min sheet.
  • Proof it computes what it says: fusionBacktest() rebuilds the history from Oura and checks 34 numbers on 7 real nights against an independent Python calculation — all 34 match.
  • Code decides: the links, the main suspect (heat is dropped when my own data shows a temperature is fine for me, even if it's above the generic 16–20 °C guideline), the action for tonight, and a complete, correct draft.
  • Nemotron explains: nvidia/Nemotron-3_5-Lightning on Nebius Token Factory rewrites the draft for a person, in strict JSON (headline, subline, why, tip), reasoning off. It never sees raw data and never does maths.
  • Code checks: every number must come from the facts; if code found no link tonight, the model must say so; the direction of the room and the body verdict are checked; one retry, then each bad field is replaced by code's text. If the model is down, the report still goes out.

Where Nemotron works

I measured the AI instead of trusting it

An evaluation harness (Evals.gs) runs 26 cases (3 real nights, 23 constructed) × 3 runs. Nemotron's report is compared with the code-only template, blind, by two judge models from different families — Nemotron-3-Ultra and Qwen3.5-397B, both on Token Factory — each pair judged in both orders. I also did a human blind review.

v1 v2
Ultra judge: Nemotron / template better 27 % / 46 % 42 % / 4 %
Qwen judge: Nemotron / template better 35 % / 54 % 38 % / 8 %
Nemotron's first answer passes all hard checks 63 % 77 %
Errors that reached the user 0 0

These numbers were measured on version 2.2. Version 3 adds the ring × room layer in code; on the 23 constructed cases the model's input, the draft and the checks are identical to 2.3, so the measured path did not change.

Challenges

  • Averages hid the effect. Nightly averages said "warm room, HRV a bit low". Only the 5-minute join showed which minutes and which sleep stages — and that inside my normal range there is no effect at all.
  • HRV changes by sleep stage by itself. A naive "warm minutes vs cool minutes" mostly measures a different mix of stages. Comparing within the same stage fixed that.
  • The first AI version lost. Judges preferred the plain template in 46 % and 54 % of cases: Nemotron chose the advice itself ("open the window" — even in a 14.8 °C room) and invented links. I gave every decision back to code; the template now wins in 4 % and 8 %.
  • Judges can be wrong: both judge models gave 5/5 for accuracy to a report that said "humidity low" at 69 %. Direction errors are now checked in code.
  • A real incident: a night recorded by Oura in pieces produced a report from an 8-minute fragment. Fragments are now merged, an afternoon nap is kept out of the night, and every run is logged.
  • A camera can't see a window in the dark, so a reed switch replaced it.

Accomplishments

  • A report built on a link no single device can show: my body and my room, every 5 minutes, compared within the same sleep stage.
  • A personal temperature map that grows every night, and a report that says "hint" until enough nights agree.
  • 34 of 34 numbers reproduced by an independent calculation.
  • A measured v1 → v2 loop for the AI layer with two independent judges, both orders and a human check; invented numbers reaching the user: 0.

What I learned

  • The value is in the join, not in either sensor: the same numbers mean nothing until they share a time axis.
  • Compare like with like — same night, same sleep stage — or the data tells you about sleep stages, not about the room.
  • Put what must never fail into code; use the model for words and judges for taste, not truth.
  • Honest limits: 7 nights with both streams, one person, one hot night carries most of the signal.

What's next

  • Turn the hint into an answer: alternate cool (≤ 21 °C) and warm (≥ 23 °C) nights for two weeks and let the map decide.
  • Bring the same 5-minute view to CO₂ and noise as more nights accumulate.
  • "Ask your room": Nemotron as a Telegram chat bot for questions about my own nights — written, not yet deployed or evaluated.
  • A PCB shield and a 3D-printed enclosure; a pilot with 5–10 Oura users.

Built With

Share this project:

Updates

Submission history