Add physics-based predicted room temperature & humidity#102
Conversation
Adds two template sensors that estimate true room temperature and humidity, correcting for the Air-1 enclosure's self-heating: - Predicted Room Temperature: weighted blend of SEN55/DPS310/SCD40 temperatures with a thermal-gradient correction (Physics Multiplier P, Baseline Offset). - Predicted Room Humidity: re-references each sensor's RH to the predicted room temperature via the Magnus formula, then blends by manufacturing tolerance. Supporting additions, all additive (stock sensors and their defaults are unchanged): - New CONFIG numbers: Physics Multiplier P, Baseline Offset, SCD40 Temperature Offset, SCD40 Humidity Offset (all default to a neutral value). - Expose SCD40 Temperature/Humidity and name the DPS310 Temperature sensor so the prediction lambdas can read them. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
These are advanced, opt-in sensors. Mark the two Predicted Room sensors, their CONFIG tuning numbers, and the newly-exposed SCD40/DPS310 inputs as disabled_by_default so the stock device page stays clean. The prediction lambdas still read the on-device state regardless of HA enablement, so functionality is unaffected when the entities are hidden. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add attribution to Ellude (Apollo Discord), who did the research and coefficient fitting. Note that the Physics Multiplier P default (0.5357) is provisional pending a better fitted value. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughCore ESPHome config adds tuning numbers, derives predicted room temperature and humidity from sensor data, exposes corrected SCD40 outputs, and disables the DPS310 temperature sensor by default. ChangesESPHome predicted-room sensing
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant sys_uptime
participant SEN55
participant DPS310
participant SCD40
participant predicted_room_temperature
participant predicted_room_humidity
sys_uptime->>predicted_room_temperature: check uptime >= 180s
SEN55->>predicted_room_temperature: provide temperature
DPS310->>predicted_room_temperature: provide temperature
SCD40->>predicted_room_temperature: provide corrected temperature
predicted_room_temperature->>predicted_room_humidity: provide predicted temperature
SEN55->>predicted_room_humidity: provide temperature and humidity
SCD40->>predicted_room_humidity: provide temperature and humidity
predicted_room_humidity-->>predicted_room_humidity: calculate and clamp humidity
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Integrations/ESPHome/Core.yaml`:
- Around line 410-415: Clamp the SCD40 humidity correction before publishing so
the lambda under humidity does not emit values outside 0-100% RH. Update the
filter in the SCD40 humidity sensor block to apply the offset in the existing
lambda and bound the result to the valid range, using the scd40_humidity and
scd40_humidity_offset identifiers to locate the change.
- Line 316: The warm-up gate in the prediction lambdas currently compares
id(sys_uptime).state directly, which can pass too early when the value is still
NaN before the first publish. Update both lambda guards that use sys_uptime to
first check std::isnan(uptime) and only allow prediction once uptime is valid
and at least 180.0, using the existing lambda logic around id(sys_uptime) so
both paths behave consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 57e0793d-adb9-4d2c-b85e-f51d15820182
📒 Files selected for processing (1)
Integrations/ESPHome/Core.yaml
Code Review: Add physics-based predicted room temperature & humidity🔴 Critical—
|
- Guard the warm-up gate against a NaN sys_uptime. sys_uptime is NaN until its first publish (~60s) and NaN < 180.0 is false, so the gate could let both prediction lambdas run before warm-up. Check isnan() first. - Extend the temperature lambda's NaN check to the coefficient/baseline values so a bad restore can't slip a NaN through the arithmetic. - Add isnan offset guards to the SCD40 temperature/humidity filters, matching the existing DPS310 pressure-offset pattern. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
Thanks for the review. Applied in 225ebdb:
Skipped a few on purpose:
|
secondof9
left a comment
There was a problem hiding this comment.
📋 Review Summary
Review Status: 🟢 APPROVED (minor warnings only, no blockers)
Change Type: ✨ New Feature
Review Effort: 🟡 Medium
Core Impact: Adds opt-in, physics-based predicted room temperature and humidity sensors to the Air-1 ESPHome config, with configurable correction coefficients.
🔍 Architectural Walkthrough
🛠️ ESPHome Configuration — Core.yaml
Integrations/ESPHome/Core.yaml- 4 new CONFIG template numbers — Physics Multiplier P, Baseline Offset, SCD40 Temperature Offset, SCD40 Humidity Offset. All
disabled_by_default,restore_value: true,entity_category: "CONFIG". Correct additive-only pattern. - Predicted Room Temperature — Weighted blend of SEN55, DPS310, SCD40 temps with thermal-gradient correction. Includes 3-minute uptime guard and full NaN propagation. Coefficients sum to ~1.0 (0.4703 + 0.3809 + 0.1488 = 1.0000).
- Predicted Room Humidity — Re-references RH via Magnus formula, blends by sensor tolerance (64% SEN55, 36% SCD40). Clamped to [0, 100].
- SCD40 Temperature & Humidity sensors — Now exposed (previously internal) with configurable offset filters.
disabled_by_default: true. - DPS310 Temperature sensor — Now exposed with name and
disabled_by_default: true(was previously unnamed/internal).
- 4 new CONFIG template numbers — Physics Multiplier P, Baseline Offset, SCD40 Temperature Offset, SCD40 Humidity Offset. All
🛠️ Critical Review & Line Suggestions
⚠️ Warning: Magnus formula denominator can hit zero (NaN propagation)
File: Integrations/ESPHome/Core.yaml
Line: Predicted Room Humidity lambda, mag_room / mag_sen / mag_scd division
The saturation vapor pressure calculation expf((17.62 * t_room) / (243.12 + t_room)) is safe for reasonable t_room values. However, 243.12 + t_room hits zero at t_room = -243.12°C — which is essentially physically impossible, but the denominator could theoretically be zero or negative if sensor data is corrupted. Division by zero yields INF, and INF / INF yields NaN. More critically, mag_room could underflow to zero (very cold room), causing division-by-zero in true_rh_sen = rh_sen * (mag_sen / mag_room).
If the predicted room temperature is extremely low, mag_room underflows to zero, and dividing mag_sen / mag_room produces INF, making true_rh_sen and true_rh_scd both INF. The final clamping at 100% masks this, but INF is a silent data integrity issue.
Suggestion:
// Calculate Saturation Vapor Pressures (Magnus Formula)
float mag_room = expf((17.62 * t_room) / (243.12 + t_room));
float mag_sen = expf((17.62 * t_sen) / (243.12 + t_sen));
float mag_scd = expf((17.62 * t_scd) / (243.12 + t_scd));
// Guard against division by zero if mag_room underflows
if (mag_room <= 0.0f) {
return NAN;
}
// Calculate Actual Room RH for each sensor
float true_rh_sen = rh_sen * (mag_sen / mag_room);
float true_rh_scd = rh_scd * (mag_scd / mag_room);
⚠️ Warning: Sensor state staleness — humidity prediction updates every 10s but depends on SEN55 at 60s
File: Integrations/ESPHome/Core.yaml
Line: Predicted Room Humidity update_interval: 10s
The humidity prediction runs every 10 seconds, but the SEN55 sensor updates every 60 seconds. This means 5 out of 6 prediction cycles recompute with stale data, wasting ESPHome lambda evaluation cycles. The temperature prediction has the same issue. Consider aligning update_interval to 60s, or at least document this staleness trade-off.
Suggestion:
update_interval: 60s # Align with SEN55 update cycle; prediction won't improve more often⚠️ Warning: SCD40 Temperature Offset cancels itself out of the temperature prediction formula
File: Integrations/ESPHome/Core.yaml
Line: SCD40 Temperature filter + Predicted Room Temperature lambda
The SCD40 Temperature sensor applies x - offset, and the temperature prediction uses t_offset = p_val * (t_scd - t_sen). Since t_scd is the offset-corrected SCD40 reading, the offset cancels out:
t_offset = p_val * ((raw_scd - offset) - t_sen) = p_val * (raw_scd - t_sen) - p_val * offset
The offset term p_val * offset is not zero — it actually does shift the prediction. This is arguably correct behavior (letting the user fine-tune), but it's a subtle coupling that's easy to misread. A brief comment explaining the interaction would prevent future confusion.
💡 Suggestion: Expose dps310temperature under consistent naming convention
File: Integrations/ESPHome/Core.yaml
New sensors follow camelCase or title_case naming (e.g., scd40_temperature), but dps310temperature is left camelCase without underscore. Consider renaming to dps310_temperature for consistency — though this is a cosmetic nit since the ID is internal-only.
💡 Suggestion: Document the coefficient source in a code comment
File: Integrations/ESPHome/Core.yaml
The weights 0.4703, 0.3809, 0.1488 and the 0.5357 multiplier are empirical. Since the PR body credits Ellude, adding a brief comment with the derivation or a link to the Discord thread would make future coefficient updates auditable.
✅ Looks Good
- NaN guard chain is thorough — Both prediction lambdas check
isnanon every dependency before computing. Theisnan(offset) ? x : x ± offsetpattern in the SCD40 filter correctly falls back to raw data when the offset entity isn't yet initialized. - Additive-only design — All new entities are
disabled_by_default, so existing Air-1 users are unaffected. The stock SEN55/SCD40/DPS310 sensors remain unchanged. - Uptime guard — The 3-minute boot delay is well-justified and properly implemented with NaN-safe comparison.
- Humidity clamping — Final RH is properly clamped to [0, 100] range.
restore_value: trueon CONFIG entities — Correct; user tuning survives device reboots.- Offset filter direction —
x - offsetmeans a positive offset reduces the sensor reading, which is correct for a sensor that runs warm (subtracting the warm bias).
|
This is very interesting. It seems like there are some milder issues that would reduce the magnitude of needed corrections and complexity and it might be good to work on those first. Just mentioning briefly as they deserve their own issues. I acknowledge this is anecdata from a limited experiment. In all of my testing, I am not enabling sleep. With all real temps 75F ish, an AIR1 from March shows an ESP temp around 100F. Two AIR1s from June show around 111F. I reflashed one of the June ones with the BLE variant and it settled around 119F. Reflashing with the base firmware (no BLE proxy, no improv) was around 100F. 25F rise is much better than 45F rise. So people that care about temperature should probably not use the BLE proxy (and just use some other proxy device), and obviously those that value BLE proxy over temp accuracy can choose that way. The sensors read unreasonably frequently in my view. Particularly given the flow from sensor value to HA to database, I see more than once a minute as not helpful. I don't believe there is real data there to read more frequently, except perhaps slightly earlier alerting of some really big excursion in VOC. The physics of air change just doesn't seem to line up with sub-minute readings. That might also reduce the temp difference. I assume that surely this PR is about "what is the rise in temp sensor X compared to the rise in ESP temp, both above ambient". |
Ultimately the best solution is buying a dedicated temp/humidity sensor and using these as air quality sensors. This PR significantly gets closer to ambient temps but we're fighting a losing battle with the heat of the microcontroller trapped in the tiny case and this just gets closer to reality not perfection. The addition of BLE would definitely use more power and therefore heat. The frequency of the sensor readings would be best triaged in an open issue on the repo - please make suggestions and I will definitely consider testing and adding them! |
|
I get it that separate is better, if it has zero cost, zero additional power requirements, and zero addtional aesthetic footprint. I was just pointing out that the BLE/improv code has a really significant power/heat cost, far more than I would have guessed. |
|
Does this need a different P value if different sensors are installed because this seems like a worse default than the default offset when compared to other reference sensors for me. It also emits nan several times on boot before things settle. |
Thanks for your comment. Can you expand on what you mean by "if different sensors are installed"? Which revision of the AIR-1 are you using? This PR specifically targets the new case design AIR-1 (the white 3d printed case) not the older white (with black mesh) style case. We have different values for that which @firstof9 should be able to help provide (he's one of our beta testers). |
|
I haven't tried this yet; I'm still going through basic calibration. A random observation that's probably true: There's deciding on constants for a particular device and the environment it is installed in and then there's having a more complicated model. Even if you still want to calibrate with this code, you may end up with a better outcome. Of course, whether you do or don't is a question of reality, and very interesting. |
|
I mean the optional mics / scd40 components. I am using the new version/case. |
|
I was able to tune my offset to using the multiplier suggested by Ellude to line up with my reference sensor without much hassle. |
|
Let me flesh out the results of my testing with some details: The issue I'm seeing is that nudging that I have two other devices currently sitting right next to it that read 23.3C temperature and 48.08% and 48.11% humidity and I've confirmed accurate temp reading with a laser thermometer on a white sheet of paper. With no offsets on anything (all set to 0.0) I'm getting prediction of 26.24C and humidity 46.9%. So I set This brings the temperature in line at 23.3 but now the humidity prediction is 55.9%. I saw this and went looking for something in the humidity prediction code that was subtracting vs adding the offset/similar but that is not how this code works at all it just uses the result of the temp prediction as input to the humidity prediction and I don't understand the math to troubleshoot further I don't think. The AIR-1 is sitting in an area with consistent air flow (within a few feet of a fan) which, if I understand what's being calculated here correctly, may affect the usefulness of this prediction method maybe but I'v turned the fan off and the behavior persists. I do have both the mics and scd40 and here's the entire config I'm using. No presence stuff. The branch referenced is just a re-published copy of this one with no changes.
Also, unrelated but...I don't understand why I need to wrap the co sensor like this, that's the floor of that sensor's measurement accuracy that or anything lower is a nonsense value afaict and I'm not sure why it's not stripped like this by default because seeing co != 0 is surprising to say the least. This is true for the no sensor too but that isn't panic inducing for me to see in a dashboard so I haven't wrapped it yet. I can open a separate issue for it though. |
Swap 17.62/243.12 (Sonntag 1990) for 17.625/243.04 per review, and cite the source in a comment. The formula is only used as a ratio, so the difference in predicted RH is under 0.02 %RH either way; this just makes the constants traceable to the cited paper. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
Yes I think both are correct enough here. I tried with this set over the weekend and didn’t see a noticable difference in the estimates I was trying to troubleshoot.On Jul 21, 2026, at 16:42, Brandon Harvey ***@***.***> wrote:
@bharvey88 commented on this pull request.
In Integrations/ESPHome/Core.yaml:
+ // Fetch the calculated Predicted Room Temperature
+ float t_room = id(predicted_room_temperature).state;
+
+ // Fetch Sensor data
+ float t_sen = id(sen55_temperature).state;
+ float rh_sen = id(sen55_humidity).state;
+ float t_scd = id(scd40_temperature).state;
+ float rh_scd = id(scd40_humidity).state;
+
+ // Ensure all sensors are actively broadcasting valid numbers
+ if (std::isnan(t_room) || std::isnan(t_sen) || std::isnan(rh_sen) || std::isnan(t_scd) || std::isnan(rh_scd)) {
+ return NAN;
+ }
+
+ // Calculate Saturation Vapor Pressures (Magnus Formula)
+ float mag_room = expf((17.62 * t_room) / (243.12 + t_room));
Thanks for checking this, and for the citation. The values I had were the Sonntag 1990 pairing (17.62 / 243.12) from the WMO Guide No. 8, so both sets are published fits, but since this lambda only ever uses the Magnus expression as a ratio the difference works out to under 0.02 %RH across realistic conditions. Swapped to the Alduchov & Eskridge 17.625 / 243.04 values in 0aa4c5e anyway and added a source comment so the constants are traceable to the paper you linked.
—Reply to this email directly, view it on GitHub, or unsubscribe.Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you commented.Message ID: ***@***.***>
|
|
note for later: we need to move this logic to its own .yaml file and change these entities to be disabled by default |
Version: deferred — no bump for now (this can sit in beta a while; set the
date-based YY.M.D.N at merge time)
What does this implement/fix?
Credit to Ellude (Apollo Discord) — they did the research and coefficient
fitting that make this work.
Adds two opt-in template sensors that estimate true room temperature and
humidity, correcting for the Air-1 enclosure's self-heating:
SCD40 temperatures with a thermal-gradient correction.
room temperature (Magnus formula), then blends by manufacturing tolerance.
Supporting CONFIG numbers for per-unit tuning:
Physics Multiplier P,Baseline Offset,SCD40 Temperature Offset,SCD40 Humidity Offset. TheSCD40 temperature/humidity and DPS310 temperature are exposed so the prediction
lambdas can read them.
Physics Multiplier Pdefaults to 0.5357, which isprovisional pending a better fitted value from Ellude.
Everything added here is additive and
disabled_by_default— the stockSEN55/SCD40/DPS310 sensors and their defaults are unchanged, and the new
entities stay hidden until a user enables them. The lambdas read on-device
state regardless of HA enablement, so disabling the entities doesn't affect the
calculation.
A wiki article covering how to enable and tune these (against a reference
thermometer) is in progress and will be linked here.
Types of changes
Checklist / Checklijst:
If user-visible functionality or configuration variables are added/modified:
🤖 Generated with Claude Code
Summary by CodeRabbit