Disclosure: I work for Apollo Automation. The ESPHome Starter Kit I tested on is one of our products.
There’s been a stalled PR in ESPHome since February that adds a pile of features to the sen6x component, the platform for Sensirion’s SEN6x air quality sensors. The size bot flagged it at over 1000 lines, two rounds of review feedback went unanswered, and the author had life get in the way. I talked to the ESPHome devs and we agreed the way forward was to redo it as smaller PRs. Another community member had also asked in the thread for five more features on top: particle counts, start/stop control, device diagnostics, fan cleaning, a reset command.
So the plan became six PRs instead of one: three stacked ones carrying the original config-time features (VOC/NOx algorithm tuning, CO2 settings, temperature compensation plus a configurable startup delay), and three independent ones for the requested extras (number concentration sensors, measurement control actions, and a device status platform). Credit for the original implementation stays with its author in every PR body.
The datasheet settled three arguments
Before writing anything I pulled the current Sensirion SEN6x datasheet and extracted the command table. That one PDF corrected three things I would otherwise have copied from the existing community code:
- The number concentration command (0x0316) works on every SEN6x variant. A community implementation had excluded the newest variant with a comment saying it “usually doesn’t provide” the data. The datasheet says it does.
- Fan cleaning and the SHT heater are idle-mode only on SEN6x. On the older SEN5x they run during measurement, so porting the SEN5x approach would have produced commands the device silently rejects.
- The VOC/NOx tuning commands only exist on four of the six variants, so the setup writes needed gating I hadn’t planned for.
Three rounds of review agents
Before filing anything I ran the code through adversarial review agents playing the role of the maintainer who reviews sen6x PRs, plus the bots that comment on every ESPHome PR. Two agents covered the six branches in round one and found real problems: back-to-back I2C writes with no spacing, a unit-conversion wraparound where a pressure sensor publishing pascals instead of hectopascals would wrap through a uint16 cast into the valid range and get silently written to the device.
Round three was a single agent on the newest model, scoped to just the diffs with the already-fixed findings listed as off-limits. It caught a bug I had introduced while fixing round one: all my “device busy” windows shared one timeout ID, and ESPHome replaces a pending timeout when you reuse its ID. On two of the variants, a stop command 2 seconds after startup would collapse a mandatory 24 second CO2 conditioning window down to 1.4 seconds. Three humans reading that code found nothing; the diff-scoped agent found it in one pass.
That felt thorough. It wasn’t enough.
Then the sensor wouldn’t even power on
I flashed the first test build onto an ESPHome Starter Kit with the SEN65 breakout module. Dead bus. “SCL is held low”, zero devices found in the I2C scan. I reseated the connector twice, power cycled it, started suspecting a broken cable, and finally said the thing that solved it: this exact kit worked fine with the stock YAML an hour ago.
The Starter Kit powers its breakout port through a GPIO-switched rail. The stock config turns that rail on at boot. My minimal test config didn’t, so the sensor had no power, and an unpowered I2C chip drags the bus lines low through its protection diodes. Every symptom I’d been debugging as a wiring problem was one missing switch block in my YAML. If a bus is mysteriously dead under a minimal config, diff it against the config that works before touching the hardware.
What the hardware actually caught
With power sorted, four flashes covered all six PRs, and two of them earned their keep immediately.
First: my config deliberately set CO2 options on the SEN65, which has no CO2 sensor, to prove the code refuses correctly. It did refuse, exactly one error line. But the config dump still printed the CO2 settings as active. The reviews had already “fixed” this once, and the fix checked a value that setup fills in asynchronously about 400ms after boot. The config dump prints at about 100ms. A race that three review passes reasoned about incorrectly showed up on the first hardware boot, on a healthy sensor, every time.
Second: the actions PR enforces timing windows, and you can’t test timing windows by clicking buttons. So the test config carried a scripted self-test that starts 20 seconds after boot and deliberately does things at the wrong time: fan cleaning 0ms after a stop (expect “Device busy”), again after 2 seconds (expect the fan to physically spin for 10 seconds, and it did), a start command inside the fan window (expect “Device busy”), and so on through nine numbered steps. Each step logs a TEST n: marker with the expected outcome, so verifying the flash is just reading down the log and matching. All nine matched.
The number concentration PR got the boring kind of proof: 85 consecutive polling cycles, 85 command writes, 85 reads, all five sensors publishing every cycle, values climbing the size ladder the way cumulative particle counts should.
One false alarm worth passing on: the device status test looked completely broken, zero publishes from any of the seven diagnostic sensors. Nothing was wrong. ESPHome’s binary_sensor logs its state publishes at VERBOSE, and my config was set to DEBUG. The reads had been running fine and invisibly the whole time.
Where it landed
All six PRs went up last night (18779 through 18784), each with a collapsed log excerpt from its flash as test evidence, plus one combined docs PR since all six features land on the same docs page. CI still found one more problem after filing: clang-tidy flagged an optional-into-optional assignment, a check none of the local hooks run. Fixed and repushed within the hour.
The honest accounting: the review agents caught bugs that reasoning about code can catch, including one a human review would probably have missed forever. The hardware caught the two bugs that only exist when real time and real electrons are involved. I tested on a SEN65, so the CO2 write path is verified against the datasheet but hasn’t touched a CO2-capable sensor yet; the PR bodies say so, and the community member who requested the features has the hardware that can close that gap. I’m hoping he does.