Skip to content

Add physics-based predicted room temperature & humidity#102

Open
bharvey88 wants to merge 4 commits into
ApolloAutomation:betafrom
bharvey88:physics-multiplier-pr
Open

Add physics-based predicted room temperature & humidity#102
bharvey88 wants to merge 4 commits into
ApolloAutomation:betafrom
bharvey88:physics-multiplier-pr

Conversation

@bharvey88

@bharvey88 bharvey88 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

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:

  • Predicted Room Temperature — weighted blend of the SEN55, DPS310 and
    SCD40 temperatures with a thermal-gradient correction.
  • Predicted Room Humidity — re-references each sensor's RH to the predicted
    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. The
SCD40 temperature/humidity and DPS310 temperature are exposed so the prediction
lambdas can read them. Physics Multiplier P defaults to 0.5357, which is
provisional pending a better fitted value from Ellude.

Everything added here is additive and disabled_by_default — the stock
SEN55/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

  • Bugfix (fixed change that fixes an issue)
  • New feature (thanks!)
  • Breaking change (repair/feature that breaks existing functionality)
  • Dependency Update - Does not publish
  • Other - Does not publish
  • Website of github readme file update - Does not publish
  • Github workflows - Does not publish

Checklist / Checklijst:

  • The code change has been tested and works locally
  • The code change has not yet been tested

If user-visible functionality or configuration variables are added/modified:

  • Added/updated documentation for the web page

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added new predicted room temperature and predicted room humidity sensors to provide a more refined, physics-based estimate (disabled by default).
  • Configuration
    • Introduced new configuration settings to tune the prediction model, including a thermal-gradient multiplier, baseline offset, and SCD40 temperature/humidity offsets.
  • Changes
    • Updated SCD40 temperature and humidity readings to apply the configured offsets.
    • Set the DPS310 temperature sensor to be disabled by default.

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)
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ed9e13b-7960-40e7-854f-7f47d5bf6fb5

📥 Commits

Reviewing files that changed from the base of the PR and between be8e447 and 225ebdb.

📒 Files selected for processing (1)
  • Integrations/ESPHome/Core.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • Integrations/ESPHome/Core.yaml

Walkthrough

Core ESPHome config adds new tuning numbers, derives predicted room temperature and humidity from sensor data, exposes corrected SCD40 outputs, and disables the DPS310 temperature sensor by default.

Changes

ESPHome predicted-room sensing

Layer / File(s) Summary
Prediction inputs
Integrations/ESPHome/Core.yaml
Adds CONFIG template numbers for coefficient_p, baseline_offset, and the SCD40 temperature and humidity offsets.
Predicted room temperature
Integrations/ESPHome/Core.yaml
Adds predicted_room_temperature with uptime gating, NaN checks, weighted temperature blending, a coefficient_p correction term, and baseline_offset.
Predicted room humidity
Integrations/ESPHome/Core.yaml
Adds predicted_room_humidity with uptime gating, NaN checks, Magnus-formula vapor pressure calculations, weighted humidity blending, and 0–100% clamping.
SCD40 outputs and DPS310 visibility
Integrations/ESPHome/Core.yaml
Adds disabled-by-default scd40_temperature and scd40_humidity template outputs that subtract the SCD40 offsets when present, and marks dps310temperature disabled by default.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

new-feature

Poem

🐰 I hopped through numbers, soft and keen,
and found a room both warm and clean.
With offsets tuned and sensors bright,
the burrow hums just right tonight.
Thump! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main addition of physics-based predicted room temperature and humidity sensors.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6a6c388 and be8e447.

📒 Files selected for processing (1)
  • Integrations/ESPHome/Core.yaml

Comment thread Integrations/ESPHome/Core.yaml Outdated
Comment thread Integrations/ESPHome/Core.yaml Outdated
@secondof9

Copy link
Copy Markdown

Code Review: Add physics-based predicted room temperature & humidity

🔴 Critical

⚠️ Warnings

  • Integrations/ESPHome/Core.yaml (SCD40 temperature filter) — The SCD40 temperature offset filter return x - id(scd40_temperature_offset).state; has no NaN guard. On first boot before restore_value completes, or if the offset entity becomes NaN, the entire SCD40 temperature chain propagates NaN silently into the Predicted Room Temperature/Humidity lambdas. The DPS310 pressure offset at line 426-427 demonstrates the correct pattern:

    float offset = id(dps310_pressure_offset).state;
    return isnan(offset) ? x : x + offset;

    The SCD40 filters should follow the same defensive pattern.

    Suggestion:

    temperature:
      name: "SCD40 Temperature"
      id: scd40_temperature
      disabled_by_default: true
      filters:
        - lambda: |
            float offset = id(scd40_temperature_offset).state;
            return isnan(offset) ? x : x - offset;
    humidity:
      name: "SCD40 Humidity"
      id: scd40_humidity
      disabled_by_default: true
      filters:
        - lambda: |
            float offset = id(scd40_humidity_offset).state;
            return isnan(offset) ? x : x - offset;
  • Integrations/ESPHome/Core.yaml (Predicted Room Temperature lambda) — The NaN guard in the temperature prediction lambda only checks sensor states (t_sen, t_dps, t_scd), but not the CONFIG coefficient states (coefficient_p, baseline_offset). If either coefficient is NaN (e.g., first boot before restore, or a corrupted restore), the arithmetic at p_val * (t_scd - t_sen) silently produces NaN which bypasses the existing guard and is returned as the final value.

    Suggestion: Extend the NaN check to include the coefficient values:

    float t_sen = id(sen55_temperature).state;
    float t_dps = id(dps310temperature).state;
    float t_scd = id(scd40_temperature).state;
    float p_val = id(coefficient_p).state;
    float baseline = id(baseline_offset).state;
    
    if (std::isnan(t_sen) || std::isnan(t_dps) || std::isnan(t_scd)
        || std::isnan(p_val) || std::isnan(baseline)) {
      return NAN;
    }
  • Integrations/ESPHome/Core.yaml (Predicted Room Humidity lambda) — Same coefficient NaN issue: the humidity lambda reads predicted_room_temperature (which could be NaN if its own coefficients are NaN), but it has no independent NaN check on its own upstream dependencies. While the current std::isnan(t_room) check catches a NaN room temperature, it doesn't distinguish why it's NaN — a sensor failure vs. a coefficient restore failure — which makes debugging harder. Consider adding a diagnostic log or at minimum ensuring the humidity lambda's NaN check is explicit about what it's guarding against.

💡 Suggestions

  • Integrations/ESPHome/Core.yaml (sys_uptime dependency) — Both prediction lambdas depend on id(sys_uptime), which is not defined in this PR. This is a cross-module dependency that could silently break if the uptime sensor is renamed or removed. Consider adding a // Requires: sys_uptime sensor comment near the top of each lambda, or better yet, adding a compile-time assertion.

  • Integrations/ESPHome/Core.yaml (clamping with min/max) — The final humidity clamping at the bottom of the humidity lambda uses an if/else block. ESPHome supports the simpler fminf(fmaxf(final_rh, 0.0f), 100.0f) idiom which is more concise and has no branching. Not a correctness issue, just a style suggestion.

✅ Looks Good

  • All new entities are disabled_by_default: true — excellent user experience; stock sensors remain untouched.
  • restore_value: true on all CONFIG number entities — state persists across restarts.
  • The 3-minute warmup guard (sys_uptime < 180.0) prevents cold-start prediction artifacts.
  • The Magnus formula usage in the humidity prediction is physically correct.
  • DPS310 temperature, SCD40 temperature, and SCD40 humidity are exposed with disabled_by_default: true so users can see raw sensor values for tuning.
  • Coefficient ranges are well-bounded (P: 0–1, Baseline: ±5°C, SCD40 offsets: ±70°C).

- 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)
@bharvey88

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Applied in 225ebdb:

  • SCD40 temp/humidity filters now follow the DPS310 isnan(offset) ? x : x - offset pattern.
  • Predicted Room Temperature NaN check now also covers coefficient_p and baseline_offset.

Skipped a few on purpose:

  • The CONFIG numbers (coefficient_p, baseline_offset, both SCD40 offsets) all set initial_value: with restore_value: true, so an ESPHome template number publishes that value on boot and is never NaN. The guards above are belt-and-suspenders rather than a live bug, but cheap enough to keep for consistency with DPS310.
  • sys_uptime lives in the same Core.yaml, not a separate module, so a // Requires: comment would be misleading. The real first-boot issue there (NaN slipping past the warm-up gate) is fixed in the same commit.
  • Left the humidity clamp as the existing if/else rather than fminf/fmaxf to keep the diff minimal; happy to switch if preferred.

@secondof9 secondof9 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 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).

🛠️ 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 isnan on every dependency before computing. The isnan(offset) ? x : x ± offset pattern 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: true on CONFIG entities — Correct; user tuning survives device reboots.
  • Offset filter directionx - offset means a positive offset reduces the sensor reading, which is correct for a sensor that runs warm (subtracting the warm bias).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants