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)
WalkthroughCore ESPHome config adds new tuning numbers, computes predicted room temperature and humidity from existing sensor data, exposes corrected SCD40 outputs, and disables the DPS310 temperature sensor by default. ChangesESPHome predicted-room sensing
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 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
| update_interval: 10s | ||
| lambda: |- | ||
| // Make sure the Air-1 is running at least for 3 minutes (avoid bad initial predictions) | ||
| if (id(sys_uptime).state < 180.0) { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files 'Integrations/ESPHome/Core.yaml'
wc -l Integrations/ESPHome/Core.yaml
sed -n '300,370p' Integrations/ESPHome/Core.yamlRepository: ApolloAutomation/AIR-1
Length of output: 2807
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files 'Integrations/ESPHome/Core.yaml'
wc -l Integrations/ESPHome/Core.yaml
sed -n '300,370p' Integrations/ESPHome/Core.yamlRepository: ApolloAutomation/AIR-1
Length of output: 2807
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files 'Integrations/ESPHome/Core.yaml'
wc -l Integrations/ESPHome/Core.yaml
sed -n '300,370p' Integrations/ESPHome/Core.yamlRepository: ApolloAutomation/AIR-1
Length of output: 2807
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "sys_uptime|uptime" Integrations/ESPHome/Core.yamlRepository: ApolloAutomation/AIR-1
Length of output: 341
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "sys_uptime|uptime" Integrations/ESPHome/Core.yamlRepository: ApolloAutomation/AIR-1
Length of output: 341
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "sys_uptime|uptime" Integrations/ESPHome/Core.yamlRepository: ApolloAutomation/AIR-1
Length of output: 341
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "sys_uptime|uptime" Integrations/ESPHome/Core.yamlRepository: ApolloAutomation/AIR-1
Length of output: 341
🌐 Web query:
ESPHome uptime sensor initial state NAN state before first update uptime platform state comparison false
💡 Result:
In ESPHome, the uptime sensor's initial state behavior depends on its configuration type and the device's synchronization status [1]. Sensors in ESPHome, including the uptime sensor, default to NaN (Not a Number) if no value has been published to the sensor's state yet [2]. Key aspects regarding uptime sensor initialization and state: 1. Initialization and "NaN" State: ESPHome sensors are initialized with NaN [2]. The uptime sensor will report this state until the first update occurs [3]. For the default type: seconds configuration, this update happens according to the update_interval (defaulting to 60 seconds) [1]. There have been historical reports of the initial state not appearing immediately in Home Assistant following a device boot, as the sensor must first perform its initial polling cycle [3]. 2. Timestamp Type Regression: If you are using type: timestamp, this mode requires an external time source (the time component) to be synchronized [1][4]. A known regression introduced in ESPHome 2026.1.0 caused the timestamp uptime sensor to report as "unavailable" (NaN) because the time synchronization callback was being skipped when the system time was already synchronized [5][6]. This issue was addressed in subsequent patches (e.g., PR #13456) to ensure the callback is always triggered [6]. 3. Platform State and state_class: While the uptime sensor has historically been assigned total_increasing as its state_class to assist with statistics in Home Assistant [7][8], this has been debated because uptime can decrease upon reboot [9]. You may see variations in how this is handled in different ESPHome versions [9][7]. If you are experiencing unexpected NaN or unavailable states, verify that: - Your update_interval is set to an appropriate duration if using seconds type [1]. - You are running a version of ESPHome that includes the fix for time synchronization callbacks if using timestamp type [5][6]. - The time component is correctly configured and successfully syncing if using timestamp type [4]. If you need the sensor to report a specific value instead of NaN before the first update, you can utilize sensor filters such as heartbeat or use publish_state() within a lambda to force an initial value upon boot [2].
Citations:
- 1: https://esphome.io/components/sensor/uptime/
- 2: https://esphome.io/components/sensor/
- 3: Initial state at reset/boot not updating in HA esphome/issues#3087
- 4: [uptime] Add new timestamp type for uptime sensor esphome/esphome#7029
- 5: Uptime timestamp sensor unavailable since 2026.1.0 esphome/esphome#13444
- 6: [time] Always call time sync callbacks even when time unchanged esphome/esphome#13456
- 7: Uptime: state_class empty esphome/issues#2337
- 8: Change uptime's state_class to total_increasing (esphome/issues#2337) esphome/esphome#2205
- 9: Uptime sensor wrongly reports itself as total_increasing class esphome/esphome#11891
🌐 Web query:
ESPHome uptime sensor initial state NAN state before first update uptime platform state comparison false
💡 Result:
In ESPHome, the uptime sensor typically reports NaN (Not a Number) or "unavailable" state before its first successful update [1]. The behavior depends on the sensor type and the state of your system: 1. Initial State Behavior: Sensors in ESPHome return NaN when accessed via get_state() or get_raw_state() if no value has been published yet [1]. The uptime sensor platform waits for its initial update interval (defaulting to 60 seconds) to publish its first value [2]. 2. Timestamp Type Regression: If you are using type: timestamp, please note that as of January 2026, some users reported the sensor showing "unavailable" (or NA) due to a regression involving time synchronization callbacks [3][4]. This was addressed in PR #13456, which ensures that time synchronization callbacks are correctly triggered even when the system time has not changed, allowing the uptime timestamp sensor to initialize properly [4]. 3. State Comparison: When writing lambdas, you should check for NaN before performing operations, as arithmetic with NaN will result in NaN. You can use std::isnan(id(uptime_sensor).state) to check this in your code. 4. Note on State Classes: The uptime sensor has historically transitioned between having no state_class and using total_increasing [5][6][7]. Because uptime can technically reset (decrease) upon a device reboot, it is sometimes debated whether total_increasing is appropriate, though it is the current standard for many uptime configurations in Home Assistant [5][8]. If your sensor remains NaN or unavailable significantly longer than the update_interval, check your logs to see if the ESPHome device is successfully connecting to the API or, in the case of the timestamp type, if the time component is successfully synchronizing [9][10].
Citations:
- 1: https://esphome.io/components/sensor/
- 2: https://esphome.io/components/sensor/uptime/
- 3: Uptime timestamp sensor unavailable since 2026.1.0 esphome/esphome#13444
- 4: [time] Always call time sync callbacks even when time unchanged esphome/esphome#13456
- 5: Uptime: state_class empty esphome/issues#2337
- 6: Uptime sensor wrongly reports itself as total_increasing class esphome/esphome#11891
- 7: Change uptime's state_class to total_increasing (esphome/issues#2337) esphome/esphome#2205
- 8: Uptime Should Return a Datetime Rather than a Counter esphome/issues#5023
- 9: Initial state at reset/boot not updating in HA esphome/issues#3087
- 10: https://esphome.io/api/uptime__timestamp__sensor_8cpp_source
🌐 Web query:
ESPHome uptime sensor initial state NAN state before first update uptime platform state comparison false
💡 Result:
In ESPHome, sensors that have not yet published a value will typically return NaN (Not a Number) when queried via lambdas like id(sensor_id).get_state or.get_raw_state [1]. The uptime sensor is a polling component, and its state is not available until the first update interval has elapsed [2][3]. Regarding the specific behaviors mentioned: Initial NaN State: Before the first update interval occurs, the sensor has no value to report [3]. If your configuration or logic relies on a valid state immediately at boot, it will encounter this NaN state [1]. The uptime sensor defaults to an update interval of 60 seconds [2]. State Comparison: If you are performing state comparisons (e.g., checking if uptime > X) in a lambda, comparing against NaN will generally result in false [1]. To handle this, you should verify the sensor has a valid state before performing comparisons, for example by checking if the value is not NaN: if (!isnan(id(uptime_sensor).state)) {... } Timestamp Type Regression: Note that a known issue in ESPHome version 2026.1.0 caused the uptime sensor with type: timestamp to report as unavailable (NaN/NA) due to a regression in time synchronization callbacks [4][5]. This was addressed in subsequent patches [5]. State Class: Historically, the uptime sensor's state_class has been a subject of discussion, with some users and PRs advocating for total_increasing and others arguing that uptime does not meet the criteria for statistical tracking as it resets on reboot [6][7][8]. If you find that your sensor remains in a NaN state indefinitely, ensure your ESP is successfully connected to the network or time source (if using the timestamp type) and that the update_interval is configured appropriately for your requirements [2][5][9].
Citations:
- 1: https://esphome.io/components/sensor/
- 2: https://esphome.io/components/sensor/uptime/
- 3: https://api-docs.esphome.io/classesphome_1_1uptime_1_1_uptime_seconds_sensor
- 4: Uptime timestamp sensor unavailable since 2026.1.0 esphome/esphome#13444
- 5: [time] Always call time sync callbacks even when time unchanged esphome/esphome#13456
- 6: Uptime: state_class empty esphome/issues#2337
- 7: Change uptime's state_class to total_increasing (esphome/issues#2337) esphome/esphome#2205
- 8: Uptime sensor wrongly reports itself as total_increasing class esphome/esphome#11891
- 9: Uptime sensor doesn't get triggered when update_interval set to a large value esphome/issues#690
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '284,365p' Integrations/ESPHome/Core.yaml
printf '\n---\n'
sed -n '840,860p' Integrations/ESPHome/Core.yamlRepository: ApolloAutomation/AIR-1
Length of output: 3479
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '284,365p' Integrations/ESPHome/Core.yaml
printf '\n---\n'
sed -n '840,860p' Integrations/ESPHome/Core.yamlRepository: ApolloAutomation/AIR-1
Length of output: 3479
Guard the warm-up gate against NaN uptime.
sys_uptime is only published every 60s and can still be NaN before its first update, so id(sys_uptime).state < 180.0 can let both prediction lambdas run too early. Check std::isnan(uptime) before comparing in both places.
Proposed fix
- if (id(sys_uptime).state < 180.0) {
+ const float uptime = id(sys_uptime).state;
+ if (std::isnan(uptime) || uptime < 180.0) {
return NAN;
}Apply the same change in both lambdas.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (id(sys_uptime).state < 180.0) { | |
| const float uptime = id(sys_uptime).state; | |
| if (std::isnan(uptime) || uptime < 180.0) { |
🤖 Prompt for 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.
In `@Integrations/ESPHome/Core.yaml` at 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.
| humidity: | ||
| name: "SCD40 Humidity" | ||
| id: scd40_humidity | ||
| disabled_by_default: true | ||
| filters: | ||
| - lambda: return x - id(scd40_humidity_offset).state; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clamp corrected SCD40 humidity before publishing.
Line 415 can emit values outside the valid 0-100% RH range, e.g. 40 - (-70) = 110.
Proposed fix
humidity:
name: "SCD40 Humidity"
id: scd40_humidity
disabled_by_default: true
filters:
- - lambda: return x - id(scd40_humidity_offset).state;
+ - lambda: |-
+ float adjusted = x - id(scd40_humidity_offset).state;
+ if (adjusted > 100.0f) return 100.0f;
+ if (adjusted < 0.0f) return 0.0f;
+ return adjusted;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| humidity: | |
| name: "SCD40 Humidity" | |
| id: scd40_humidity | |
| disabled_by_default: true | |
| filters: | |
| - lambda: return x - id(scd40_humidity_offset).state; | |
| humidity: | |
| name: "SCD40 Humidity" | |
| id: scd40_humidity | |
| disabled_by_default: true | |
| filters: | |
| - lambda: |- | |
| float adjusted = x - id(scd40_humidity_offset).state; | |
| if (adjusted > 100.0f) return 100.0f; | |
| if (adjusted < 0.0f) return 0.0f; | |
| return adjusted; |
🤖 Prompt for 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.
In `@Integrations/ESPHome/Core.yaml` around lines 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.
Code Review: Add physics-based predicted room temperature & humidity🔴 Critical—
|
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
New Features
Changes