Disclosure: I work for Apollo Automation. The AIR-1 in this post is one of our devices, and its firmware is part of my job.
A few AIR-1 owners reported the same thing: every reading worked except NOx. Their logs all had this pair of lines:
[D][sen5x:125]: Type: SEN54
[E][sen5x:139]: NOx requires a SEN55
The module inside an AIR-1 is a Sensirion SEN55. It says so on the sticker. But ESPHome doesn’t read stickers, it asks the module what it is over I2C at boot, and on these units the module answers SEN54. A SEN54 has no NOx element, so ESPHome shuts the NOx sensor off. Reasonable behavior, wrong answer.
NOx never worked on these units
The check that disables NOx on a SEN54-reporting module has been in the ESPHome component since its first commit in May 2022, so this isn’t something an update broke. The log wording users were pasting is newer, from the 2026.2.0 release, which is probably why the reports started showing up: the message got short and clear enough to notice. But on these units, NOx had never published a value. Nobody caught it for years because a sensor that’s disabled and a sensor that never publishes look identical in Home Assistant: one entity, permanently unavailable.
Was the hardware lying or the register?
The SEN54 and the SEN55 share a case, an interface, and firmware. The difference is which sensing elements are physically populated, and the product name is programmed at the factory. So a module that answers SEN54 might be a mislabeled SEN55, or it might actually be a SEN54 under a SEN55 sticker, with no NOx element at all.
We buy these modules through DigiKey. That’s what made this confusing: the lazy explanation, somebody relabeling cheaper SEN54s as SEN55s, doesn’t hold up when the modules come from an authorized distributor. And you can’t settle it by reading datasheets. You have to ask the NOx element itself.
The test was a small patch to the sen5x component: a model: option that overrides what the module reports. Force model: sen55, let the NOx sensor run, and the hardware answers the question for you. Real numbers after the warm-up period means the element is there and the register is wrong. Permanently unknown means it really is a SEN54 inside.
My own AIR-1 turned out to be one of the affected units. I forced the model, waited out the conditioning period, and got real NOx values. The register is wrong. The hardware is fine.
Shipping it twice
The fix went out on two tracks. The workaround: the patched component lives in a repo our firmware can pull with an external_components: block, and the AIR-1 beta config forces model: sen55. On units that report SEN55 correctly, the override matches and changes nothing.
The real fix: a PR to ESPHome adding model: as a proper config option, with a docs PR to match. Filed and merged the same day, tagged for the 2026.7.2 patch release. Once that ships, the vendored component gets deleted and everything this post describes collapses into one line of YAML: model: sen55.
What three rounds of review taught me
Most of what I learned came out of code review.
Every unique log string costs flash on every device that compiles the component. My first version had two long warning messages explaining that forced readings might be invalid. A bot posted the bill, 449 bytes, and a maintainer pointed me at the logging guidelines. The final message is Detected SEN54, using SEN55. The explanation moved to the documentation, which is where they tell you it belongs.
The same principle killed my setup error logs: they repeated information the component already reports through its failed-state config dump. Deleted, not shortened.
And one Windows-specific lesson: the docs lint failed because my edit round-tripped the file through Python on Windows, which quietly converted every line ending to CRLF. The linter’s error message was clear about it. My terminal was not.
If your AIR-1 shows those two log lines, the workaround is on beta now, and the real fix is in ESPHome 2026.7.2 when it ships.