Skip to content

refactor: Extract thermal learning into utils/thermal_learning.py#1947

Open
cl445 wants to merge 2 commits intoKartoffelToby:developfrom
cl445:refactor/phase1-thermal-learning
Open

refactor: Extract thermal learning into utils/thermal_learning.py#1947
cl445 wants to merge 2 commits intoKartoffelToby:developfrom
cl445:refactor/phase1-thermal-learning

Conversation

@cl445
Copy link
Contributor

@cl445 cl445 commented Feb 24, 2026

Motivation

climate.py is by far the largest file in the project at 3729 lines. calculate_heating_power() (253 lines) and calculate_heat_loss() (131 lines) are self-contained state machines scattered across ~20 loose self.* attributes. This causes three concrete problems:

  1. Not testable in isolation — the learning logic is inseparable from the HA entity lifecycle. Unit tests need a full BetterThermostat mock just to verify an EMA calculation.
  2. Fragile state — 16 loose attributes without shared context (heating_start_temp, loss_end_timestamp, …) are error-prone. There is no guarantee they are set/reset consistently.
  3. Hard to review — changes to the thermal logic require navigating a 3700-line file with completely unrelated code around it.

This PR addresses all three by extracting the logic into typed dataclass state machines that are testable without Home Assistant.

Summary

  • Extract calculate_heating_power() (253 LOC) and calculate_heat_loss() (131 LOC) from climate.py into typed dataclass state machines in new utils/thermal_learning.py
  • Reduce climate.py by ~282 lines (3729 → 3447) — both methods become thin wrappers (~25 + ~15 lines)
  • Add 54 unit tests for the new module with full coverage of helpers, tracker transitions, finalization, EMA smoothing, clamping, and telemetry

Details

New: utils/thermal_learning.py (~490 lines)

  • HeatingPowerTracker / HeatLossTracker — stateful dataclasses with update() returning frozen result objects
  • CycleResult, HeatingPowerUpdate, HeatLossUpdate — frozen result dataclasses (no HA side-effects)
  • Pure helpers: ema_smooth(), clamp(), compute_weight_factor(), compute_env_factor()
  • Zero HA runtime imports (HVACAction via TYPE_CHECKING only)
  • mypy --strict clean

Changes in climate.py

  • 16 loose self.* thermal attributes replaced by self._heating_tracker + self._loss_tracker
  • 7 compatibility properties (heating_power, heat_loss_rate, heating_cycles, etc.) — documented with TODO for future elimination
  • New _get_outdoor_temp() helper method (6 lines)

Other changes

  • events/window.py: 1-line update for tracker API (self._heating_tracker.start_temp = None)
  • tests/unit/test_climate_baseline.py: Updated fixture to use real tracker objects

Test plan

  • 54/54 new test_thermal_learning.py tests pass
  • 63/63 baseline test_climate_baseline.py tests pass
  • 991/991 full test suite passes
  • mypy --strict clean on thermal_learning.py (0 errors)
  • Manual smoke test with real HA instance
Add 63 regression tests covering the 6 most important methods in
climate.py before refactoring begins. Uses the unbound-method pattern
with MagicMock fixtures, consistent with existing test conventions.

Classes:
- TestShouldHeatWithTolerance (10 tests)
- TestComputeHvacAction (15 tests)
- TestCalculateHeatingPower (12 tests, async)
- TestCalculateHeatLoss (9 tests, async)
- TestAsyncSetPresetMode (7 tests, async)
- TestAsyncSetTemperature (10 tests, async)
@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

@cl445 cl445 changed the base branch from master to develop February 24, 2026 08:43
@cl445 cl445 force-pushed the refactor/phase1-thermal-learning branch 3 times, most recently from bb3d939 to fbe82cf Compare February 24, 2026 08:56
@cl445 cl445 marked this pull request as ready for review February 24, 2026 09:02
@cl445 cl445 force-pushed the refactor/phase1-thermal-learning branch from fbe82cf to 8b15e57 Compare February 24, 2026 09:05
…ase 1)

Extract calculate_heating_power() (253 lines) and calculate_heat_loss()
(131 lines) from climate.py into typed, mypy-strict-clean dataclass
state machines (HeatingPowerTracker, HeatLossTracker).

climate.py shrinks by ~282 lines (3729 → 3447). The two methods become
thin wrappers (~25 and ~15 lines) that delegate to the trackers and
handle HA side-effects via frozen Result dataclasses.

- New: utils/thermal_learning.py (~490 lines) with full type annotations
- New: tests/unit/test_thermal_learning.py (54 tests)
- Updated baseline tests to use real tracker objects
- Updated events/window.py for tracker API
- All 991 tests pass, mypy --strict clean on thermal_learning.py
@cl445 cl445 force-pushed the refactor/phase1-thermal-learning branch from 8b15e57 to 6f0324d Compare February 24, 2026 09:06
@cl445 cl445 changed the title refactor: Extract thermal learning into utils/thermal.py (Phase 1) Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant