Skip to content

Latest commit

 

History

History
167 lines (134 loc) · 8.24 KB

README.md

File metadata and controls

167 lines (134 loc) · 8.24 KB

A bedroom sensor node, in ESPHome

Seven sensor ICs on one ESP32-S3, reporting into Home Assistant: CO₂, particulates, VOC, spectral light, lux, temperature, humidity, pressure, and — if you build that part — a sound level that is never a recording.

It is aimed at a room you sleep in, which is a different problem from a room you work in. Blue light matters at night and much less by day; a cooling fan is a defect rather than a detail; and a microphone in a bedroom has to be one that cannot listen.

What has actually been run

This matters more than the rest of the README, so it goes first.

Everything in bedroom-node.yaml is built and running, microphone included. The node has reported continuously since 2026-06-25: all seven sensors plus sound_level, publishing peak and average loudness about once a second. An earlier build had the Bluetooth proxy enabled and no microphone — that is not what runs now, and the two have still never been enabled together.

What the sound level is and is not good for. The floor in a quiet bedroom sits near -41 dBFS and discrete noises stand out clearly, so it trends disruption well. It cannot tell you what made a noise. Six weeks of it could not separate breathing from a passing truck, and no amount of resolution on a single loudness number ever will — that needs classification, and classification needs the audio itself.

Which is why the node can now stream. See Audio streaming below — and note that streaming is not opt-in: the config switches it on at 21:00 and off at 10:00 every night, so the node records roughly thirteen hours out of twenty-four with nobody touching it. That trades away the property that made the microphone easy to justify.

The second node described in PARTS.md — for a room used in the daytime — was planned and never made.

Three things worth knowing before you order

Three parts commonly ship as the wrong part. SCD40 sold as SCD41. SGP30 sold as SGP40. BMP280 sold as BME280 — one letter, and the BMP has no humidity sensor at all. Listings routinely offer both variants under one title with the wrong one as default. This is the most useful thing in the repo; see PARTS.md.

A chunk of GPIOs on an N16R8 board is unusable. The octal PSRAM die is physically wired to GPIO 33–37 whether or not you enable PSRAM in software (these configs do not). They are broken out on the header and look perfectly available.

GPIO Why not
0, 3, 45, 46 strapping pins (boot mode, VDD_SPI)
19, 20 USB-OTG D− / D+
26–32 not broken out — internal SPI flash
33–37 octal PSRAM on R8 boards
43, 44 USB-UART bridge

Audio and BLE do not coexist well on one node. ESPHome warns that running its Bluetooth proxy alongside audio components causes instability, and this config keeps the microphone with the proxy commented out because of that warning. The honest limit of the claim: it was never tested with both enabled — one build had the proxy and no mic, the current one has the mic and no proxy. It is a documented upstream caveat that shaped this config, not something measured here.

Full pin assignments, the I2C address map and pull-up values are in WIRING.md.

Running it

cp secrets.yaml.example secrets.yaml     # your WiFi, and two passwords you invent
esphome run bedroom-node.yaml

Where the live node is actually flashed from: not this checkout. It is built and pushed by the ESPHome container on the homelab box, which keeps its own copy of the config and its own secrets.yaml holding the OTA password. Treat that copy as the deployed truth and this one as the source of record; if they drift, diff them before flashing anything.

Three configs, meant to be climbed in order rather than picked between:

bedroom-node-bare.yaml WiFi, logging, OTA. Proves the board boots and joins.
bedroom-node-minimal.yaml Adds one I2C sensor. Proves the bus and your wiring.
bedroom-node.yaml Everything.

Framework and logger settings are identical across all three, so each rung adds exactly one variable. Going straight to the full config on a fresh breadboard means debugging power, wiring, the bus and the config at once.

All three set min_version. The config uses sound_level, bme280_i2c and the list form of ota:, which arrived in different ESPHome releases — without the version floor you get three unrelated validation errors and none of them says "upgrade".

Never commit a compiled .bin. ESPHome writes your WiFi password and OTA password into the firmware image in plain text, readable with strings. .gitignore covers it.

Audio streaming

The audio_stream component in components/ ships raw PCM to a server over TCP. receiver/ is the other half: a small service that pipes the stream into ffmpeg and writes Opus segments to disk.

This changes what the node is. With streaming off it is a device that cannot record you. With streaming on it is a microphone in your bedroom feeding a server that keeps the audio. Both are legitimate; they are not the same device, and the config should never make that switch quietly. Hence: a switch in Home Assistant that always says which device it currently is, a schedule written in plain sight in the config rather than buried in an automation, and a daytime boot that comes back not recording.

It records nightly on a clock, not on demand. Streaming turns on at 21:00 and off at 10:00 every day (time: → on_time in bedroom-node.yaml), which is thirteen hours of every twenty-four with no human action. If the node reboots inside that window it resumes recording on its own once time syncs — otherwise a power blip at 03:00 would end the night in silence, because on_time fires at an instant and that instant has passed. A boot outside the window comes back not recording. The Home Assistant switch overrides the schedule in either direction until the next trigger fires.

Design decisions worth knowing before you change any of them:

  • The microphone source is passive. The streamer never starts or stops the microphone — it rides along with sound_level, which owns it. Adding streaming therefore cannot break sensing that already works, which is the only reason it was safe to add to a node that had run for six weeks.
  • The ESP stores nothing. About two seconds of ring buffer, and then the samples are on the network or they are gone. Under backpressure it discards the oldest audio so the stream stays current, and logs every dropped byte. An ESP32 pretending to be a storage device is a worse failure than a gap.
  • The host must be a literal IP. A hostname would mean a blocking DNS lookup on the main loop every reconnect — a stall on a node also driving seven sensors, and one nobody would attribute to the right cause. The config validator refuses names outright.
  • Segments are five minutes, not an hour. ffmpeg's segment muxer does not reliably honour -flush_packets, so a segment only reaches disk when it closes. Segment length is the bound on what a crash costs.
  • Gain is +12 dB. A quiet bedroom measures about -68 dB mean on this microphone, and the 32-to-16-bit conversion discards resolution exactly where the quiet detail lives. Measured peaks sit near -52 dB, leaving ample headroom.

Bandwidth is 32 kB/s on the wire (16 kHz, 16-bit, mono), landing at roughly 7 kB/s once encoded at 48 kbit/s — about 340 MB for a thirteen-hour night, and close to 10 GB across the 30-day window the receiver prunes to.

Known rough edges

  • No API encryption. api: has no encryption: key, so Home Assistant talks to the node in the clear on your LAN. Add a key if that matters to you.
  • The fan duty-cycle is an unfinished sketch. It is the right answer to the only thing here that will wake you up, and it does not validate as written — see the comment in the config.
  • The status LED is on GPIO48, correct for the board revision this was built against. Espressif has moved it between revisions; check yours.
  • PSRAM is not enabled. Nothing here needs it. Worth knowing if you add on-device audio work later, which is why an S3 was chosen.

Status

Published as reference, not maintained. One node, one room, running. MIT — see LICENSE.