-
Notifications
You must be signed in to change notification settings - Fork 34
Add physics-based predicted room temperature & humidity #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bharvey88
wants to merge
6
commits into
ApolloAutomation:beta
Choose a base branch
from
bharvey88:physics-multiplier-pr
base: beta
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+201
−0
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
9732d3b
Add physics-based predicted room temperature & humidity
bharvey88 387af30
Disable predicted-room entities by default
bharvey88 be8e447
Credit Ellude and mark multiplier as provisional
bharvey88 225ebdb
Harden prediction lambdas against NaN inputs
bharvey88 0aa4c5e
Use Alduchov & Eskridge 1996 Magnus coefficients
bharvey88 dd20dfc
Move room prediction logic into Predicted_Room.yaml
bharvey88 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| # Physics-based room temperature/humidity prediction. | ||
| # Research and coefficient fitting by Ellude (Apollo Discord). | ||
| # | ||
| # The Air-1 enclosure self-heats, so the on-board sensors read warmer/drier | ||
| # than the room. This package adds two opt-in template sensors that estimate | ||
| # the true room temperature and humidity, plus the CONFIG numbers used to tune | ||
| # them per unit. Everything here is additive and disabled by default; the | ||
| # stock SEN55/SCD40/DPS310 sensors and their defaults are unchanged. | ||
| # | ||
| # Included from Core.yaml. Relies on ids defined there: sys_uptime, | ||
| # sen55_temperature, sen55_humidity, scd40, dps_310, dps310temperature. | ||
|
|
||
| number: | ||
| - platform: template | ||
| name: "Physics Multiplier P" # thermal-gradient multiplier | ||
| id: coefficient_p | ||
| disabled_by_default: true | ||
| restore_value: true | ||
| # Provisional value from Ellude; update once a better fit is available. | ||
| initial_value: 0.5357 | ||
| min_value: 0.0000 | ||
| max_value: 1.0000 | ||
| entity_category: "CONFIG" | ||
| optimistic: true | ||
| update_interval: never | ||
| step: 0.0001 | ||
| mode: box | ||
|
|
||
| - platform: template | ||
| name: "Baseline Offset" | ||
| id: baseline_offset | ||
| disabled_by_default: true | ||
| restore_value: true | ||
| initial_value: 0.0 | ||
| min_value: -5.0 | ||
| max_value: 5.0 | ||
| entity_category: "CONFIG" | ||
| unit_of_measurement: "°C" | ||
| optimistic: true | ||
| update_interval: never | ||
| step: 0.01 | ||
| mode: box | ||
|
|
||
| - platform: template | ||
| name: SCD40 Temperature Offset | ||
| id: scd40_temperature_offset | ||
| disabled_by_default: true | ||
| restore_value: true | ||
| initial_value: 0.0 | ||
| min_value: -70.0 | ||
| max_value: 70.0 | ||
| entity_category: "CONFIG" | ||
| unit_of_measurement: "°C" | ||
| optimistic: true | ||
| update_interval: never | ||
| step: 0.1 | ||
| mode: box | ||
|
|
||
| - platform: template | ||
| name: SCD40 Humidity Offset | ||
| id: scd40_humidity_offset | ||
| disabled_by_default: true | ||
| restore_value: true | ||
| initial_value: 0.0 | ||
| min_value: -70.0 | ||
| max_value: 70.0 | ||
| entity_category: "CONFIG" | ||
| unit_of_measurement: "%" | ||
| optimistic: true | ||
| update_interval: never | ||
| step: 0.1 | ||
| mode: box | ||
|
|
||
| sensor: | ||
| # Room temperature prediction. Blends the three on-board temperature sensors | ||
| # and corrects for enclosure self-heating using the Physics Multiplier P / | ||
| # Baseline Offset CONFIG numbers above. | ||
| - platform: template | ||
| name: "Predicted Room Temperature" | ||
| id: predicted_room_temperature | ||
| disabled_by_default: true | ||
| unit_of_measurement: "°C" | ||
| device_class: temperature | ||
| state_class: measurement | ||
| accuracy_decimals: 2 | ||
| update_interval: 10s | ||
| lambda: |- | ||
| // Make sure the Air-1 is running at least for 3 minutes (avoid bad initial predictions). | ||
| // sys_uptime is NAN until its first publish, and NAN < 180.0 is false, so guard for it. | ||
| float uptime = id(sys_uptime).state; | ||
| if (std::isnan(uptime) || uptime < 180.0) { | ||
| return NAN; | ||
| } | ||
|
|
||
| // Fetch Sensor data and coefficients | ||
| 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; | ||
|
|
||
| // Ensure all sensors and coefficients are valid numbers before predicting | ||
| if (std::isnan(t_sen) || std::isnan(t_dps) || std::isnan(t_scd) || | ||
| std::isnan(p_val) || std::isnan(baseline)) { | ||
| return NAN; | ||
| } | ||
|
|
||
| // Estimate the temperature inside Air-1 | ||
| float t_base = (0.4703 * t_sen) + (0.3809 * t_dps) + (0.1488 * t_scd); | ||
|
|
||
| // Estimate the required offset | ||
| float t_offset = p_val * (t_scd - t_sen); | ||
|
|
||
| // Return predicted room temperature | ||
| return t_base - t_offset + baseline; | ||
|
|
||
| # Room humidity prediction. Re-references each sensor's RH to the predicted | ||
| # room temperature (Magnus formula), then blends by manufacturing tolerance. | ||
| - platform: template | ||
| name: "Predicted Room Humidity" | ||
| id: predicted_room_humidity | ||
| disabled_by_default: true | ||
| unit_of_measurement: "%" | ||
| device_class: humidity | ||
| state_class: measurement | ||
| accuracy_decimals: 1 | ||
| update_interval: 10s | ||
| lambda: |- | ||
| // Make sure the Air-1 is running at least for 3 minutes (avoid bad initial predictions). | ||
| // sys_uptime is NAN until its first publish, and NAN < 180.0 is false, so guard for it. | ||
| float uptime = id(sys_uptime).state; | ||
| if (std::isnan(uptime) || uptime < 180.0) { | ||
| return NAN; | ||
| } | ||
|
|
||
| // 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, coefficients | ||
| // from Alduchov & Eskridge 1996). Used only as ratios, so the 6.112 | ||
| // prefactor cancels and is omitted. | ||
| float mag_room = expf((17.625 * t_room) / (243.04 + t_room)); | ||
| float mag_sen = expf((17.625 * t_sen) / (243.04 + t_sen)); | ||
| float mag_scd = expf((17.625 * t_scd) / (243.04 + t_scd)); | ||
|
|
||
| // 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); | ||
|
|
||
| // Lowest statistical variance based on sensor manufacturing tolerances (64% SEN55, 36% SCD40) | ||
| float final_rh = (0.64 * true_rh_sen) + (0.36 * true_rh_scd); | ||
|
|
||
| // Clamp to valid range | ||
| if (final_rh > 100.0) { | ||
| final_rh = 100.0; | ||
| } else if (final_rh < 0.0) { | ||
| final_rh = 0.0; | ||
| } | ||
|
|
||
| return final_rh; | ||
|
|
||
| # Expose the raw sensors the prediction reads from, with tuning offsets, | ||
| # by extending the hardware sensor blocks defined in Core.yaml. | ||
| - id: !extend scd40 | ||
| 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; | ||
|
|
||
| - id: !extend dps_310 | ||
| temperature: | ||
| name: "DPS310 Temperature" | ||
| disabled_by_default: true | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift
Inverted
!extenddirection betweenCore.yamland the new package. The package extendsscd40anddps_310, but those ids are declared in the file that includes the package. ESPHome resolves!extendonly from the including configuration toward package-provided ids.Integrations/ESPHome/Predicted_Room.yaml#L175-L196: remove the two!extendblocks, or move thescd4xanddps310platform definitions into this package so the ids originate here.Integrations/ESPHome/Core.yaml#L10-L13: if you keep the base sensor definitions inCore.yaml, place the!extend scd40and!extend dps_310blocks inCore.yamlafter thepackages:include.Validate the chosen layout with
esphome config Integrations/ESPHome/AIR-1.yaml.📍 Affects 2 files
Integrations/ESPHome/Predicted_Room.yaml#L175-L196(this comment)Integrations/ESPHome/Core.yaml#L10-L13🤖 Prompt for AI Agents