fix: Reduce false degraded-mode warnings during startup - #1907
Merged
KartoffelToby merged 3 commits intoFeb 12, 2026
Conversation
Optional sensors (outdoor, weather, window, humidity) frequently need seconds to initialise after a Home Assistant reboot. The previous startup code checked them once, immediately logged WARNING-level messages, and set degraded_mode=True — even though the sensors came online moments later and degraded mode self-resolved. Changes: - Remove the premature per-sensor WARNING checks and the "Starting in DEGRADED MODE" message from startup entirely - Add a retry loop with increasing delays (3/5/10/15/25 s, ~60 s max) that waits for optional sensors before the first check_and_update_degraded_mode() call — exits early as soon as all sensors are available - Let check_and_update_degraded_mode() be the single source of truth for degraded mode (it already handles logging, HA issues, and state transitions correctly) The room-temperature-sensor fallback (semi-critical) retains its WARNING because it represents a genuinely degraded state. Closes KartoffelToby#1902
Contributor
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ 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 |
Extract the startup retry loop into a testable await_optional_sensors() function in watcher.py with: - Injectable _sleep parameter for deterministic testing - Configurable delay sequence (default 3/5/10/15/25 s) - Final check after last sleep to catch late arrivals - Returns list of still-pending entity IDs Add 9 unit tests covering: - All sensors available immediately (no sleep) - No optional sensors configured - Sensor comes online after retry - All retries exhausted (returns pending sensors) - Default delays are strictly increasing and sum to ~60 s - Custom delay overrides - Partial sensor availability (some online, some not) - Final check catches sensors that arrive during last sleep
Owner
|
@cl445 one test failed. |
KartoffelToby
added a commit
that referenced
this pull request
Jun 2, 2026
* pid percent rounding
* mpc loss fix
* mpc loss fix
* trvzb valve maintenance interval
* fix: ignore TRV target temp changes during active communication (#1882)
* fix: ignore TRV target temp changes during active communication
When BT sends commands to a TRV, it sets `ignore_trv_states = True` to
prevent the TRV's response from being misinterpreted. However, in the
temperature change handler in events/trv.py, this flag was not checked.
This caused TRV responses (or internal TRV logic like Tado scheduling)
to override `bt_target_temp` even while BT was actively communicating
with the TRV.
This fix adds the missing check for `ignore_trv_states` in the condition
that decides whether to adopt a temperature change from the TRV.
Fixes #1733
* test: add tests for ignore_trv_states flag in temperature change logic
Tests verify that temperature changes from TRVs are correctly blocked
when ignore_trv_states is True during active BT communication.
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* fix: change HEAT to HEAT_MODE if not in available modes (#1880)
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* fix: don't update cooler state if no change is needed (#1879)
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* fix: translate HEAT_COOL mode on TRVs when AC entity is connected (#1878)
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* [TASK] fix lint
* [TASK] fix lint
* [TASK] fix lint
* [TASK] fix test
* Fix issue with translated entities use translation_key (#1896)
* [TASK] make sure the auto find entites are work with multilang
* [TASK] fix test
* feature/dynamic entity cleanup (#1897)
* [BUMP] version
* docs: Add AI development credits to feature request
- Clearly document that this feature was implemented with Claude AI assistance
- Credit AI collaboration for codebase analysis, architecture design, and implementation
- Highlight comprehensive nature of AI-assisted development process
- Provide transparency about AI involvement in the feature development
* fix markdown errors
* fix markdown errors
* fix: Resolve merge conflict in sensor.py - Integrate MpcKaSensor with dynamic entity management
- Integrated BetterThermostatMpcKaSensor from master branch
- Preserved complete dynamic entity management system from feature branch
- Updated MPC entity tracking to include new Ka sensor (5 total sensors)
- Removed redundant has_mpc logic in favor of universal algorithm detection
- All MPC entities (including Ka) now properly managed by cleanup system
- Seamless algorithm switching with automatic entity lifecycle management
Breaking: None
Compatibility: Full backward compatibility maintained
Testing: MPC algorithm diagnostics enhanced with thermal insulation monitoring
Resolves merge conflict between feature/dynamic-entity-cleanup and master
Addresses CodeRabbit merge conflict detection in sensor.py
* fix: Resolve merge conflict in sensor.py - Integrate MpcKaSensor with dynamic entity management
- Integrated BetterThermostatMpcKaSensor from master branch
- Preserved complete dynamic entity management system from feature branch
- Updated MPC entity tracking to include new Ka sensor (5 total sensors)
- Removed redundant has_mpc logic in favor of universal algorithm detection
- All MPC entities (including Ka) now properly managed by cleanup system
- Seamless algorithm switching with automatic entity lifecycle management
Breaking: None
Compatibility: Full backward compatibility maintained
Testing: MPC algorithm diagnostics enhanced with thermal insulation monitoring
Resolves merge conflict between feature/dynamic-entity-cleanup and master
Addresses CodeRabbit merge conflict detection in sensor.py
* fix: Address CodeRabbit review issues - Fix signal format, memory leaks, and grammar
- Fix signal format inconsistency: Use entry_id instead of entity_id in climate.py
- Fix memory leak: Store and use dispatcher unsubscribe functions properly
- Add _DISPATCHER_UNSUBSCRIBES tracking for proper cleanup on unload
- Fix grammar: Change 'Can be aggressive initially' to 'PID can be aggressive initially'
Issues resolved:
- Major: Signal format alignment between climate.py and sensor.py
- Major: Dispatcher callbacks now properly unsubscribed to prevent accumulation
- Minor: Grammar improvement in PID controller documentation
All CodeRabbit requested changes have been addressed.
* fix: Address CodeRabbit review issues - Fix signal format, memory leaks, and grammar
- Fix signal format inconsistency: Use entry_id instead of entity_id in climate.py
- Fix memory leak: Store and use dispatcher unsubscribe functions properly
- Add _DISPATCHER_UNSUBSCRIBES tracking for proper cleanup on unload
- Fix grammar: Change 'Can be aggressive initially' to 'PID can be aggressive initially'
Issues resolved:
- Major: Signal format alignment between climate.py and sensor.py
- Major: Dispatcher callbacks now properly unsubscribed to prevent accumulation
- Minor: Grammar improvement in PID controller documentation
All CodeRabbit requested changes have been addressed.
* fix: Resolve CodeRabbit review issues - Add missing MpcKaSensor class and fix signal handling
- Add missing BetterThermostatMpcKaSensor class implementation in sensor.py
* Fixes critical NameError when MPC calibration mode is active
* Monitors MPC thermal insulation coefficient (Ka parameter)
* Follows established pattern of other MPC sensors (Gain, Loss)
- Fix signal format in climate.py _signal_config_change method
* Change self._entry_id to self._config_entry_id (correct attribute)
* Aligns with signal listener expectations in sensor.py
* Ensures proper config change propagation to entity cleanup handlers
- Add .vscode/settings.json to .gitignore to exclude from public repo
Resolves all critical and major issues identified by CodeRabbit in PR #1887
* chore: Remove .vscode/settings.json from Git tracking
- File should remain local only per .gitignore configuration
* fix: Resolve critical async/await and data structure issues in sensor.py
- Fix async_get_entity_registry call: add 'await' keyword
* Was being called as sync function but returns a coroutine
* This prevented proper entity registry initialization
- Change bt_climate.all_trvs to bt_climate.real_trvs
* all_trvs doesn't exist in the codebase
* real_trvs is the correct dict of loaded TRV instances
* Fixes AttributeError in _get_active_algorithms
- Update loop to handle dict.items() from real_trvs
* Changed from 'for trv in all_trvs' to 'for trv_id, trv in real_trvs.items()'
* Ensures proper unpacking of dictionary data structure
These fixes ensure:
- Entity registry operations complete successfully
- Algorithm detection works with actual TRV data
- No AttributeError at runtime when checking calibration modes
* fix: Use sync entity registry accessor in sensor cleanup
* fix: Log invalid calibration modes in sensor algorithm detection
* fix: Align MPC Ka sensor unit with changelog
* fix: Keep algorithm tracking when entity cleanup is partial
* feat: Add comprehensive cleanup for unused preset, PID number and switch entities
- Add automatic cleanup for unused preset input numbers (requested by @wtom)
- Extend cleanup system to PID number entities (Kp, Ki, Kd parameters)
- Add cleanup for PID auto-tune switch entities
- Implement entity tracking in number.py and switch.py platforms
- Enhance sensor.py with unified cleanup functions for all dynamic entities
- Add proper unload functions for tracking cleanup
- Maintain compatibility with existing algorithm sensor cleanup
Addresses code owner feedback in PR #1887 for complete entity management.
Co-authored-by: Claude AI <claude@anthropic.com>
* fix: Run entity cleanup on all config changes, not just algorithm changes
Addresses CodeRabbit feedback: preset cleanup was only triggered when algorithms
changed, causing stale preset number entities to remain when users only disabled
presets without changing calibration modes.
- Move _cleanup_unused_number_entities() outside algorithm-change conditional
- Ensure preset/PID number cleanup runs on every configuration change signal
- Maintains performance by only triggering on actual config changes via dispatcher
Fixes: https://github.com/KartoffelToby/better_thermostat/pull/1887#discussion_r1734567890
* fix: Use consistent TRV data source and key access in PID cleanup functions
- Both PID number and switch cleanup now use bt_climate.real_trvs (dict)
- Both use CONF_CALIBRATION_MODE constant instead of hardcoded string
- Ensures cleanup functions work on same TRV dataset consistently
- Addresses CodeRabbit feedback about inconsistent data sources
* docs: Add clarifying comment about TRV data source consistency
- Explicitly note that PID number cleanup uses same data source as switch cleanup
- Helps clarify the consistency fix from previous commit
- Addresses any remaining questions about TRV data source alignment
* fix: Normalize CalibrationMode values before PID comparison
- Add proper string-to-enum conversion in PID calibration checks
- Handle string-configured modes correctly in sensor.py cleanup functions
- Apply same normalization fix to switch.py PID creation logic
- Use CONF_CALIBRATION_MODE constant consistently in switch.py
- Prevents comparison issues between raw strings and enum values
* fix: Guard against None preset_modes in cleanup function
- Add null safety check for bt_climate.preset_modes before set conversion
- Use empty list as fallback when preset_modes is None or falsy
- Prevents TypeError when presets are unsupported by the climate entity
- Maintains existing logic for removing 'none' preset from cleanup scope
* feat: Extend switch cleanup to handle child lock switches
- Add cleanup logic for orphaned child lock switches when TRVs are removed
- Parse both _pid_auto_tune and _child_lock suffixes from switch unique IDs
- Remove child lock switches for TRVs that no longer exist in real_trvs
- Use same entity registry removal logic for consistent cleanup behavior
- Update logging to reflect cleanup of both switch types
- Addresses CodeRabbit feedback for comprehensive switch entity management
* feat: Add native SELECT entity support for HomeMatic temperature offset
- Extend find_local_calibration_entity() to discover SELECT temperature_offset entities
- Add fallback search for SELECT entities when device-based search fails
- Modify get_current_offset() to strip 'k' suffix from SELECT values (e.g., '1.5k' -> 1.5)
- Update set_offset() to detect entity domain and use appropriate service:
* SELECT entities: Use select.select_option with 'k' suffix formatting
* NUMBER entities: Continue using number.set_value (backward compatibility)
- Enhance get_min_offset() and get_max_offset() to parse options list for SELECT entities
- Support HomeMatic HM-CC-RT-DN thermostats via homematicip_local integration
- Eliminates need for template NUMBER entity workarounds
- Maintains full backward compatibility with existing NUMBER entity setups
- Resolves GitHub issue #1888
* Update custom_components/better_thermostat/adapters/generic.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* fix: Improve SELECT entity handling robustness in set_offset
- Make domain derivation null-safe using entity_id when entity_state is None
- Add comprehensive option validation for SELECT entities
- Handle missing entity_state gracefully by using empty options list
- Improve closest option matching with robust error handling
- Parse each option individually to handle malformed options
- Only call select_option with validated options from the entity
- Fallback to original option_value if parsing fails
- Enhances reliability for HomeMatic SELECT temperature offset entities
* docs: Fix markdown violations and add HomeMatic SELECT documentation
- Fix formatting in CHANGELOG_PRESET_NUMBER_CLEANUP.md and CLEANUP_REVIEW_SUMMARY.md
- Improve README.md structure and heading hierarchy
- Add comprehensive HomeMatic IP/CCU SELECT entity documentation
- Document temperature_offset activation steps for HomeMatic users
- Fix markdown compliance across all documentation files
* Update docs/Configuration/configuration.md
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Update CHANGELOG_PRESET_NUMBER_CLEANUP.md
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* fix: Correct spelling and formatting in CLEANUP_REVIEW_SUMMARY.md
- Fix German compound hyphenation: Debug/Info Logging → Debug-/Info-Logging
- Remove trailing whitespace after 'activity' in test scenarios
- Address CodeRabbit spelling/typography issues
* docs: Translate CLEANUP_REVIEW_SUMMARY.md to English for consistency
- Translate German content to English to match rest of codebase
- Maintain consistent language across all documentation files
- Keep technical details and formatting intact
- Ensure professional documentation standards
* chore: rollback to stable state and update manifest/gitignore
- Rollback to commit 743ae5a before version 1.8.0-dev update
- Revert defensive NoneType fixes that caused integration instability
- Update manifest.json with correct version
- Adjust .gitignore for development workflow
* fix: Use lazy formatting in watcher logger call
Replace f-string with lazy %s formatting in DEBUG logger call to avoid
eager evaluation cost when log level is disabled. Aligns with all other
log calls in the module.
* remove individual unused files
* fix: improve exception handling specificity in climate.py
- Replace broad 'except Exception' catches with specific exception types
- Improves debuggability and prevents false exception masking
- Addresses 12 instances across multiple operation contexts:
* Model/device detection: AttributeError, TypeError
* Storage init/load (PID/MPC/TPI/Thermal): FileNotFoundError, PermissionError, RuntimeError
* External temperature operations: OSError, RuntimeError, AttributeError, TypeError
* Async task creation: RuntimeError
* EMA initialization: ValueError, TypeError, ImportError
* Heat loss calculations: ValueError, TypeError, KeyError
Changes maintain backward compatibility while improving error handling granularity.
* style: fix lazy logging and trailing whitespace
- Replace f-string with lazy % formatting in logger calls
- Remove trailing whitespace across 11 files
- Improves logging performance (lazy evaluation)
- No functional changes
Files: generic.py, config_flow.py, cooler.py, window.py, BTH-RM.py, TV02-Zigbee.py, number.py, sensor.py, switch.py, helpers.py, test_off_temperature_attribute.py
* fix: address CodeRabbit cleanup and add pylint config
* style: ignore unused-argument warnings in pylintrc
Callback handlers in Home Assistant often have unused arguments (e.g., event
parameter in dispatcher callbacks). This is intentional and not an error.
* style: fix unused variables in sensor.py and mpc.py
- Replace unused 'trv_id' loop variables with underscore (5 occurrences)
- Remove unused 'solar_gain_factor' variable assignment
* style: fix line length violations
* style: remove trailing whitespace and reorganize imports
* config: ignore import-outside-toplevel warnings (C0415) for tests
* config: ignore protected-access warnings (W0212)
* config: restrict pylint to custom_components directory only
* config: restrict linting to custom_components only (pylint + pyright)
* config: improve pyright ignore patterns for tmp and other directories
* fix: remove pyright include to preserve package structure
* fix: add missing __init__.py to adapters package
* fix: add missing __init__.py files to all Python packages
* fix: resolve pylint/pylance errors in helpers.py
- Add @staticmethod decorators to rounding class methods
- Fix round_by_step to properly default to rounding.nearest
- Add None checks for config_entry_id before API calls
- Add None check for entry in get_trv_intigration
- Fix device_id handling in find_valve_entity
* style: fix pylint/pylance infos and improve code quality
- Remove unnecessary pass statements in exception classes
- Initialize heating_cycles and heating_power_normalized in __init__ to prevent lazy initialization
- Replace inefficient .keys() iterations with direct dict iteration (Python 3 best practice)
- Change .items() iterations for direct access to dict values (performance improvement)
- Add debug logging to asyncio.CancelledError handler for better observability
- Move import statement to proper location after module aliases
- Add module docstrings to simulator files (pid_simulator, mpc_simulator)
All changes address pylint/pylance info messages while maintaining backwards compatibility and code stability.
* fix: add None-safety guards for thermal_store and attribute access
- Add explicit None checks before float() conversions for ATTR_TEMPERATURE, ATTR_STATE_HEATING_POWER, and ATTR_STATE_HEAT_LOSS
- Guard _thermal_store.async_load() calls to satisfy Pyright type checking
- Store .get() values in intermediate variables to improve type inference
- Resolves 6 Pyright None-safety errors while maintaining defensive programming patterns
* fix: add humidity state safety check and optimize type annotations
- Add None check for humidity_state.state access at line 862 (fixes .state type error)
- Import cached_property from functools for proper type compatibility
- Change device_info decorator from @property to @cached_property to match Entity base class signature
- Improves defensive programming for Home Assistant state access patterns
All None-safety errors now resolved (7/7).
* chore: update .gitignore with comprehensive Python and development patterns
- Add Python bytecode patterns (*.pyc, *.pyo, *.py[cod])
- Add Python package build artifacts (dist/, build/, *.egg-info/)
- Expand virtual environment patterns (ENV/, env/, .env)
- Add testing artifacts (.pytest_cache/, .tox/, htmlcov/)
- Add IDE patterns (sublime, swap files)
- Add OS-specific files (Thumbs.db, Desktop.ini)
- Add Home Assistant database and log files
- Add temporary and backup file patterns
- Organize with category comments for better maintainability
- Keep manifest and spec files (needed for packaging)
* fix: restore broken target temperature state recovery
- Revert redundant double None-check in ATTR_TEMPERATURE restore logic
- The additional safety check in else branch caused state restore to fail
- This led to 'Undefined target temperature, falling back to 7.0' and
HVAC mode defaulting to OFF, preventing MPC calibration from running
- Restores original working logic that was modified in previous commits
Fixes Better Thermostat state restore regression causing MPC sensors
to remain unknown after restart due to disabled calibration.
* fix: add missing exception handling for heating power state restore
- Add try/catch (TypeError, ValueError) around float() conversion for ATTR_STATE_HEATING_POWER
- Matches existing exception handling pattern used for heat loss restore
- Prevents startup crashes when corrupted heating power values are stored
- Ensures consistent error handling across all state restore operations
Fixes potential ValueError crash during startup sequence when non-numeric
heating power values (e.g. 'unknown') are encountered in saved state.
* [MERGE]
* [MERGE]
---------
Co-authored-by: Mulatz Florian (IT OS IC WA GO) <florian.mulatz@infineon.com>
Co-authored-by: Florian Mulatz <florian@mulatz.at>
Co-authored-by: Florian Mulatz <33655308+florianmulatz@users.noreply.github.com>
Co-authored-by: Claude AI <claude@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Fix aggressive calibration not applied in hysteresis band (#1823)
* Fix aggressive calibration not applied in hysteresis band
Change the condition for applying aggressive calibration from checking
hvac_action == HEATING to checking cur_temp < bt_target_temp.
The previous condition failed in the hysteresis band (between target-tolerance
and target) because:
- When temperature is in this band and system was previously IDLE
- The hysteresis logic keeps hvac_action as IDLE
- So aggressive calibration adjustment was not applied
- Result: TRV temperatures stayed "too close" to target
The fix ensures aggressive calibration is applied whenever the room actually
needs heating (cur_temp < target), regardless of the hysteresis-driven
hvac_action state.
Closes #1790
* Fix aggressive calibration by skipping tolerance delay
The real issue was that aggressive calibration mode was subject to the
same tolerance delay as DEFAULT mode. When hvac_action is IDLE and
heating should start, the tolerance delay (tolerance * 2.0) was added
to the calibration, effectively making aggressive mode LESS aggressive
when starting to heat.
Fix: Skip the tolerance delay for AGGRESIVE_CALIBRATION mode while
keeping the -2.5 offset behavior unchanged (only applies when HEATING).
This addresses the root cause identified in wtom's review comment.
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* [TASK] implement code suggestions
* mpc claude changes
* trv compensation
* [TASK] code fixes
* fix: implement proper HA availability pattern for MPC sensors (#1899)
- Add @property available() to all MPC sensor classes
- Return False when thermostat is OFF, window is open, or climate not available
- This ensures sensors show 'unavailable' instead of 'unknown' per HA best practices
- Affected sensors: virtual_temperature, mpc_gain, mpc_loss, mpc_insulation_ka
Resolves issue where MPC sensors showed 'unknown' state inappropriately.
* fix: Prevent complex number crash in heating_power_valve_position (#1870)
* fix: Prevent complex number crash in heating_power_valve_position
Fixes crash when heating_power_valve_position is called with negative
temperature difference (cur_temp > target_temp).
This occurs in rare TRV override edge case:
1. System heating normally (cur_temp < target_temp)
2. Fast temperature rise (sun, external heat)
3. TRV reports "heating" with delay
4. _compute_hvac_action() overrides to HEATING
5. Function called with negative temp_diff
6. Formula produces complex number: (-x) ** 0.946
7. TypeError on comparison
Changes:
- Add guard for temp_diff <= 0 at function start
- Return 0.0 (no heating needed) when room warmer than target
- Add debug logging for this edge case
- Add comprehensive tests (extracted from #1868)
Related to #1868 (test coverage PR)
Affects: HEATING_POWER_CALIBRATION mode only
* test: Remove meta-comments from test docstrings
* style: fix ruff formatting in test_helpers_valve_position
* refactor: Clean up test docstrings and remove meta-communication
Remove bug-hunting language ("FIXED:", "BUG:", severity ratings),
try/except pytest.fail patterns, and unused pytest import. Rename
test methods to describe expected behavior instead of referencing bugs.
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* fix: Add boost mode support for available TRVs (#1873)
* fix: Add boost mode support for available TRVs
**Problem:**
Boost mode logic (lines 296-313) only existed in the unavailable TRV path.
When a TRV was available (normal case), boost mode had no effect.
**Root Cause:**
Code duplication between unavailable (lines 262-582) and available (584-829)
TRV paths led to missing boost mode implementation in the available path.
**Fix:**
Added boost mode logic to available TRV path (after line 611):
1. Set temperature to max_temp when boost mode is active (lines 613-619)
2. Set valve to 100% when boost mode is active (lines 625-632)
This mirrors the existing boost mode logic from the unavailable path.
**Related Issues:**
- Fixes #1817 - Support native operation modes (Boost/Vacation/Profiles)
- Users reported boost mode not working - this explains why it only worked
when TRV was unavailable (which is the exceptional case, not the norm)
**Testing:**
Added test_boost_mode_bug.py with tests documenting the bug and verifying
that boost mode now works for both unavailable and available TRVs.
* test: Clean up test and add missing bt_hvac_mode
- Remove meta-comments from docstrings (BUG, After the fix)
- Add missing bt_hvac_mode attribute to mock
- Rename test to describe behavior, not bug
- Clean up assertions
* fix: Move set_valve call outside elif to fix boost mode
- Critical bug: set_valve was inside elif block, never executed for boost mode
- Moved set_valve logic after if/elif/else so it executes for both boost and calibration
- Fixed in both unavailable TRV path (lines 307-397) and available TRV path (lines 625-710)
- Updated tests with proper mocks (adapter, model_quirks, cooler_entity_id, etc.)
- All 3 boost mode tests now pass
* test: Fix import ordering in test_boost_mode_bug.py
* fix: Reset valve when safety overrides force HVAC OFF in boost mode
When boost mode sets valve to 100% but safety checks (window open or
call_for_heat=False) force HVAC mode to OFF, the valve was left at 100%.
This created conflicting commands that could cause unpredictable TRV behavior.
Now explicitly reset valve to 0% when safety overrides activate during boost.
Addresses CodeRabbit review feedback on PR #1873.
* refactor: Extract valve control helpers and fix safety override
- Add _is_boost_heating_active() to check boost condition
- Add _get_valve_control() to determine valve settings from boost or calibration
- Add _apply_valve_control() to apply valve settings to TRV
- Add _reset_valve_on_safety_override() to reset valve when HVAC forced OFF
- Remove redundant cooler section from unavailable TRV path (handled by control_cooler)
- Reduce code duplication between unavailable and available TRV paths
- Add tests for safety override scenarios (window open, no heat call)
- Rename test file from test_boost_mode_bug.py to test_boost_mode.py
* fix: Remove unused solar_gain_factor variable
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* fix: Handle TypeError in check_float for None and invalid types (#1869)
* fix: Handle TypeError in check_float for None and invalid types
Fixes bug where check_float() raised TypeError instead of returning
False when passed None or invalid types (list, dict, etc.).
The function only caught ValueError but not TypeError, which is raised
when float() is called with incompatible types.
Changes:
- Add TypeError to exception handling in check_float()
- Add comprehensive tests (extracted from #1868)
Related to #1868 (test coverage PR)
Fixes bug #2 documented in #1868
Tests:
- test_returns_false_for_none ✅
- test_returns_false_for_invalid_types ✅
- All 6 tests pass
* test: Remove unused pytest import
* refactor: Remove meta-communication from test module docstring
* style: Fix ruff format
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* [TASK] cleanup code
* max open percentage as input number
* lint
* fix: import_mpc_state_map drops string and int fields during state restore (#1904)
* fix: import_mpc_state_map drops string and int fields during state restore
import_mpc_state_map uses float() as default coercion for all fields
not explicitly handled. This silently drops string fields like
trv_profile ("threshold", "linear", etc.) because float("threshold")
raises ValueError, which is caught and the field is skipped.
Additionally, profile_samples (int) was not listed in the int-coercion
branch, causing it to be coerced to float instead of int.
Changes:
- Add trv_profile to str() coercion branch
- Add profile_samples to int() coercion branch
- Add tests covering round-trip for all field types
* refactor: Use match-case for type coercion in import_mpc_state_map
* fix: Add type annotation for coerced variable in match-case
mypy infers the type from the first case arm only. Restore the
explicit union annotation so all branches type-check correctly.
* fix: add missing match-cases for bool/int fields and extend round-trip tests
Address Copilot review feedback:
- Add match-cases for regime_boost_active (bool) and
consecutive_insufficient_heat (int) so they survive import with
correct types instead of being coerced to float.
- Add round-trip tests for perf_curve (dict), regime_boost_active,
and consecutive_insufficient_heat.
- Include all new fields in the full_state_round_trip test.
- Clean up double import of mpc module.
---------
Co-authored-by: Wolfgang Tom <wtom@users.noreply.github.com>
* fix: Detect regime change when all prediction errors are identical nonzero (#1905)
* fix: Detect regime change when all prediction errors are identical nonzero
When all recent prediction errors are identical and nonzero (std=0),
_detect_regime_change returned False because the t-test division by
std is undefined. However, identical nonzero errors represent a
perfectly consistent bias that should trigger a regime change.
Return mean_error != 0 instead of False when std_error == 0.
* test: replace flaky random.gauss test with fixed deterministic series
Address Copilot review: test_high_variance_masks_small_bias used
random.gauss which made it non-deterministic and only asserted the
return type. Replace with a fixed error series (mean ≈ 0.01,
std ≈ 0.39) and assert the expected False result.
* fix: Extend lock coverage to prevent race conditions in parallel TRV control (#1875)
* fix: Skip all control operations for unavailable TRVs
After PR #1813, the code logged "skipping control" for unavailable TRVs but
still executed all operations (Lines 271-582), including:
- convert_outbound_states()
- set_valve()
- set_hvac_mode()
- set_offset()
- set_temperature()
This was wasteful and could cause issues when these operations failed.
Changed to return True immediately after detecting unavailable TRV without
executing any operations. The TRV state change event will trigger a new control
cycle when it becomes available.
Removed 311 lines of unnecessary code execution for unavailable TRVs.
* fix: Extend lock coverage to prevent race conditions in parallel TRV control
## Problem
When control_queue() runs control_trv() in parallel for multiple TRVs
(using asyncio.gather), race conditions occur because _temp_lock only
protected lines 240-257, leaving critical operations unprotected:
- set_valve() (line 358)
- set_hvac_mode() (line 430)
- set_offset() (line 492)
- set_temperature() (line 509)
This caused issues with grouped TRVs where both TRVs access shared state
(self.cur_temp, self.bt_target_temp, etc.) simultaneously.
## Fix
Extended the lock scope to protect ALL critical operations from line 240
to line 518 (end of function). Now when multiple TRVs are controlled in
parallel:
- TRV1 acquires lock
- TRV1 completes ALL operations
- Lock is released
- TRV2 acquires lock
- TRV2 completes ALL operations
Operations are now serialized, preventing race conditions.
## Test Coverage
Added test_race_condition_lock_coverage.py with 3 tests:
1. test_parallel_trv_control_no_race_condition: Verifies no interleaving
2. test_shared_state_corruption_in_parallel_execution: Checks state integrity
3. test_lock_protects_critical_sections: Confirms lock is held during operations
Test results show operations now run sequentially instead of interleaving.
## Related Issues
Fixes:
- #1839: Automation stuck on climate.set_temperature (2 grouped TRVs)
- #1145: Grouped TRVs automatically turn off
- #875: Grouped thermostats turned to 30°C
- #1733: BT changes Target Temp with no input from user
- #1849: Random target temperature changes
All these issues share the same root cause: race conditions when controlling
multiple TRVs in parallel without proper synchronization.
* test: Fail test when race condition is detected
- Replace early return with AssertionError
- Test now properly fails if operations interleave
- With fix in place, no race condition should occur
* fix: Remove unused imports and fix import ordering
- Remove unused PRESET_BOOST import from controlling.py
- Fix import ordering in test_race_condition_lock_coverage.py
- Fix import ordering in test_unavailable_trv_no_operations.py
- Remove unused HVACMode import
Note: test_shared_state_corruption_in_parallel_execution has a pre-existing failure (KeyError: 'adapter')
* docs: Add missing docstrings to controlling.py
Add docstrings to control_trv, control_queue, control_cooler,
TaskManager class, handle_window_open, check_system_mode,
and check_target_temperature functions.
* fix: Address CodeRabbit review comments
- Add early guard for heater_entity_id before accessing real_trvs
- Remove duplicate calculate_heating_power() call and _trv fetch
- Move asyncio.sleep(3) outside lock scope to allow parallel TRV control
- Add None/unavailable check in check_target_temperature()
- Update tests to force state mismatch for critical operation coverage
- Patch all delegate functions in tests to avoid retry delays
* docs: Add docstrings to test helper functions
* refactor: Remove meta-communication from tests and code comments
Remove bug tracking references (Bug #5), GitHub issue numbers, PR
references, and process-oriented language from test docstrings and
inline comments. Keep technical descriptions of what is being tested.
* style: Fix ruff format
* refactor: clean up meta-communication comments in controlling.py
Remove change-history comments, commented-out code, developer names,
and process-oriented descriptions. Keep only comments that describe
what the code does.
* test: Comprehensive MPC controller test coverage (#1903)
* test: add comprehensive MPC controller tests (105 tests, 3 expected failures)
Covers helper functions, state persistence, core control logic,
adaptive learning, virtual temperature, regime change detection,
TRV profile detection, post-processing, perf curve sampling,
forced calibration, stale state detection, sibling seeding,
and edge cases.
3 tests fail and expose real bugs:
- import_mpc_state_map drops string fields (trv_profile)
- _detect_regime_change returns False for constant nonzero errors
* test: adapt MPC tests to develop, add group-key + distribute tests
- Rebase on develop, fix time() vs monotonic() mismatch
- Rewrite virtual-temp tests for Kalman-based API (no invalid kwargs)
- Remove test_std_zero_returns_false (tested buggy behavior as correct)
- Add 4 tests for build_mpc_group_key (multi-TRV group key)
- Add 10 tests for distribute_valve_percent (TRV compensation)
- 115 passed, 3 failed (3 documented bugs in production code)
* test: add 34 tests covering Kalman filter, analytical solver, and learning paths
Cover develop's new MPC subsystems:
- Kalman filter: P init/predict/update, gain K at high/low P, physics predict
- Analytical solver: zero-gain fallback, cost debug, negative error
- max_opening_pct clamping
- gain_learn_count >= 2 guard for warm_low_u loss learning
- Gain recovery path (over-correction upward)
- High-u steady-state gain learning
- ka_est outdoor-dependent dynamic loss initialization and update
- Residual rate-limiting boundaries (300s, 3600s)
- Big-change force-open bypass (hold-time bypass for large increases only)
- Deque eviction (maxlen=20)
- Stale state anchor reset
- Target change 0.05K boundary
- distribute_valve_percent negative input
- Solar gain initialization
- Integration accumulation and window-open reset
Total: 152 tests (149 pass, 3 expected failures from documented bugs).
* test: address Copilot review feedback
- Fix docstring: replace "time is mocked via monkeypatch" with accurate
description (real time with large deltas)
- Fix error message in test_valve_monotonically_increases_with_error:
use actual temps array instead of incorrect formula
- Clean up double import of mpc module
* test: Add comprehensive unit tests for helpers.py (#1868)
* test: Add comprehensive unit tests for helpers.py functions
Add 87 new unit tests for helper functions in helpers.py to improve
test coverage and document existing bugs:
New test files:
- test_helpers_normalize.py (34 tests): Tests for calibration mode,
HVAC mode normalization, and float conversion functions
- test_helpers_rounding.py (22 tests): Tests for rounding helpers
and float validation with floating-point precision edge cases
- test_helpers_valve_position.py (16 tests): Tests for heating power
valve position calculation including edge case bug documentation
- test_helpers_mode_remap.py (16 tests): Tests for HVAC mode remapping
between Better Thermostat and TRVs with quirk handling
Bugs found and fixed in separate PRs:
1. [MEDIUM-HIGH] heating_power_valve_position crashes with complex
numbers in TRV override edge case (negative temp_diff)
→ Fixed in PR #1870
2. [HIGH] check_float raises TypeError for None/invalid types instead
of returning False (missing TypeError catch)
→ Fixed in PR #1869
3. [NOT A BUG] convert_to_float rounds values < 0.005 to 0 - this is
by design (PR #1805 fixed #1792, #1789, #1785). The 0.01 step
rounding preserves sensor precision while preventing artifacts.
Tests updated to reflect this is expected behavior.
4. [MEDIUM] rounding enum methods have incorrect epsilon behavior
→ Documented in tests, not critical
5. [LOW] Floating-point precision issues in round_by_step
→ Documented in tests, cosmetic only
Related PRs:
- #1869: Fix check_float TypeError (extracted tests from this PR)
- #1870: Fix heating_power_valve_position edge case (extracted tests from this PR)
- #1867: Fix rounding enum (already merged)
All tests pass. Critical bugs (#1, #2) are fixed in separate PRs.
* test: Remove meta-comments from rounding tests
* test: Remove remaining meta-comments from test_helpers_rounding.py
* test: Remove meta-comments and fix ruff linter issues
- Renamed test_bug_* to test_* (removed 'bug' prefix)
- Fixed docstrings to describe behavior without meta-language
- Removed unused 'result' variables
- All ruff checks pass (except pre-existing mpc.py issue)
- All tests pass
* test: fix tests to assert correct expected behavior instead of current bugs
check_float should return False for None/invalid types, not raise TypeError.
heating_power_valve_position should return 0.0 for negative temp diffs, not
produce complex numbers. Tests now fail until the corresponding fixes land.
* refactor: Remove meta-communication from test docstrings
Remove PR references, "BY DESIGN" labels, issue numbers, and
bug-hunting try/except patterns. Replace with objective descriptions
of expected behavior. Remove unused pytest import.
* style: Fix ruff format issues
* fix: Improve type hints for mypy compliance (#1867)
* fix: Improve type hints and add mypy CI enforcement
This commit fixes all mypy type errors and enforces type checking in CI
to prevent future type regressions.
- **pid.py**: Created PIDDebugInfo TypedDict for proper debug dict typing (19 dict-item errors)
- **mpc.py**: Added None check for state.loss_est before division (1 arg-type error)
- **model_fixes/default.py**: Define STATE_LOCKED/UNLOCKED locally as they don't exist in HA const (2 attr-defined errors)
- **switch.py**: Added explicit list[SwitchEntity] type annotation (1 arg-type error)
- **number.py**: Added explicit list[NumberEntity] type annotation (3 arg-type errors)
- **utils/helpers.py**:
- Changed rounding class from Enum to static methods
- Added Callable type hint for f_rounding parameter (5 errors)
- **utils/weather.py**: Created typed list for valid temperatures before sum() (1 arg-type error)
- **device_trigger.py**: Added proper return type with dict[str, bool] for metadata (1 return-value error)
- **device_condition.py**: Fixed function signatures to use Mapping[str, object] | None (2 return-value errors)
- **config_flow.py**:
- Avoided TypedDict ** expansion by explicitly passing arguments
- Added type annotations for trv_bundle and updated_config (3 errors)
- **utils/helpers.py**:
- `normalize_calibration_mode`: Changed from Any to CalibrationMode | str | None
- `is_calibration_mode`: Changed from Any to CalibrationMode | str | None
- **config_flow.py**:
- `_as_bool`: Changed from Any to bool | str | int | None
- `_duration_dict_to_seconds`: Changed from Any to int | float | dict[str, int] | None
- `_seconds_to_duration_dict`: Changed from Any to int | float | str | None
- `add_entity_selector`: Changed domain from Any to str | list[str]
- **utils/calibration/tpi.py**: `_round_dbg`: Changed from Any to float | int | None
- **utils/calibration/mpc.py**: `_round_for_debug`: Changed from Any to float | int | None
- Added `.github/workflows/mypy.yml` to enforce type checking on push and PRs
- Workflow uses Python 3.13 and uv for dependency management
- All 41 source files now pass mypy type checking
- Verified all mypy errors are resolved (0 errors in 41 files)
- Confirmed existing tests still pass (25 passed, 97 pre-existing errors)
- No new test failures introduced by type hint changes
* fix: Address PR review comments
- Fix mypy workflow to use system-installed mypy directly instead of `uv run mypy`
This ensures mypy can see the packages installed with `uv pip install --system`
- Modernize lock state handling to use LockState enum instead of string constants
This follows Home Assistant's current best practices and improves type safety
* docs: Add docstrings to helper functions in config_flow.py
Add docstrings to resolve(), add_field(), add_entity_selector(),
get_value(), and get_bool() closure functions within _build_user_fields
and _build_advanced_fields.
* fix: Remove mypy CI workflow and fix type annotations for develop compatibility
Remove the separate mypy.yml CI workflow. Fix type annotations in
sensor.py, switch.py, number.py, and config_flow.py to pass mypy
after rebasing on develop's entity-cleanup changes. Use assert
statements to narrow _attr_unique_id types and proper generic types
instead of Any for module-level tracking dicts.
* style: Fix ruff lint and format issues
* fix: Add type annotations and None guards for sensor.py mypy compliance
* [TASK] fix timeout and tolerance bugs (#1909)
* [TASK] fix timeout and tolerance bugs
* [TASK] fix testing and lint
* mpc tolerance - new overshoot penalty instead of eco penalty
* 10% close hold bypass
* lint
* fix: Reduce false degraded-mode warnings during startup (#1907)
* fix: Reduce false degraded-mode warnings during startup
Optional sensors (outdoor, weather, window, humidity) frequently need
seconds to initialise after a Home Assistant reboot. The previous
startup code checked them once, immediately logged WARNING-level
messages, and set degraded_mode=True — even though the sensors came
online moments later and degraded mode self-resolved.
Changes:
- Remove the premature per-sensor WARNING checks and the
"Starting in DEGRADED MODE" message from startup entirely
- Add a retry loop with increasing delays (3/5/10/15/25 s, ~60 s max)
that waits for optional sensors before the first
check_and_update_degraded_mode() call — exits early as soon as all
sensors are available
- Let check_and_update_degraded_mode() be the single source of truth
for degraded mode (it already handles logging, HA issues, and state
transitions correctly)
The room-temperature-sensor fallback (semi-critical) retains its
WARNING because it represents a genuinely degraded state.
Closes #1902
* refactor: Extract await_optional_sensors() and add tests
Extract the startup retry loop into a testable
await_optional_sensors() function in watcher.py with:
- Injectable _sleep parameter for deterministic testing
- Configurable delay sequence (default 3/5/10/15/25 s)
- Final check after last sleep to catch late arrivals
- Returns list of still-pending entity IDs
Add 9 unit tests covering:
- All sensors available immediately (no sleep)
- No optional sensors configured
- Sensor comes online after retry
- All retries exhausted (returns pending sensors)
- Default delays are strictly increasing and sum to ~60 s
- Custom delay overrides
- Partial sensor availability (some online, some not)
- Final check catches sensors that arrive during last sleep
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* [TASK] fix duplicate tests
* test: Add unit tests for utils/controlling.py (#1871)
* test: Add unit tests for utils/controlling.py
Add 5 new test files covering the controlling module (61 tests):
- test_controlling_control_cooler.py — control_cooler()
- test_controlling_control_queue.py — control_queue()
- test_controlling_control_trv.py — control_trv()
- test_controlling_helpers.py — handle_window_open(), check_system_mode(),
check_target_temperature()
- test_controlling_task_manager.py — TaskManager
Tests adapted to current develop API:
- control_cooler now sends set_temperature before set_hvac_mode
- Unavailable TRVs trigger early-return skip (use available state in tests)
* test: Restructure controlling tests into tests/unit/controlling/ package
Move all utils/controlling.py tests into a dedicated package for better
organization. Absorb scattered test files and deduplicate:
- test_control_trv.py: merged from test_boost_mode.py (safety overrides),
test_race_condition_lock_coverage.py, test_grouped_trv_calibration.py,
and test_unavailable_trv_no_operations.py (72 tests total)
- test_control_queue.py: moved (15 tests)
- test_helpers.py: merged with handle_window_open tests from
test_window_no_off_mode.py (16 tests)
- test_control_cooler.py: moved (9 tests)
- test_task_manager.py: moved (5 tests)
Removed test_control_queue_retry.py (documentation-only pass stubs).
Kept test_window_no_off_mode.py for events/trv.py tests only.
* fix: resolve mypy errors in test_mpc_comprehensive.py (#1913)
Replace **kwargs dict-unpacking for MpcParams/MpcInput dataclasses
with explicitly typed function signatures. mypy cannot match
`dict[str, object]` to individual dataclass field types.
- _default_params: list all MpcParams fields with correct types,
use _UNSET sentinel + dataclasses.replace for optional overrides
- _inp: list all MpcInput fields directly, construct dataclass
without dict-unpacking
* refactor: Add unified StateManager for runtime state persistence (#1914)
* refactor: Add unified StateManager for runtime state persistence
Introduce a single versioned HA Store per config entry that replaces
the four separate Store files for MPC, PID, TPI, and thermal data.
- Add state_manager.py with RuntimeState, MpcState, PidState, TpiState,
ThermalStats dataclasses and typed serialization/deserialization
- Add StateManager class with dirty-flag tracking, get-or-create
accessors, and async load/save/flush lifecycle
- Add schema migration path (_migrate_v0_to_v1) for future versioning
- Full mypy --strict compliance (zero type: ignore)
- 58 unit tests covering roundtrip, type coercion, edge cases, and
StateManager lifecycle
* fix: harden _deserialize against corrupted store data
- Guard mpc/pid/tpi sections with isinstance(…, Mapping) before
calling .items(), so non-dict payloads don't crash deserialization
- Wrap float() conversion for thermal stats in try/except so
non-numeric values yield None instead of raising
* Fix: Enable decimal temperature input for preset number entities (#1922)
* fix: implement proper HA availability pattern for MPC sensors
- Add @property available() to all MPC sensor classes
- Return False when thermostat is OFF, window is open, or climate not available
- This ensures sensors show 'unavailable' instead of 'unknown' per HA best practices
- Affected sensors: virtual_temperature, mpc_gain, mpc_loss, mpc_insulation_ka
Resolves issue where MPC sensors showed 'unknown' state inappropriately.
* Fix: Enable decimal input for temperature presets
- Fixed preset temperature number entities to support decimal values (e.g., 23.5°C)
- Updated TEMP_STEP_SELECTOR to include backwards compatibility for existing "0.0" configs
- Modified target_temperature_step handling to treat both "" and "0.0" as auto mode (None)
- Set default step of 0.1°C for preset number entities when auto mode is enabled
- Resolves validation errors preventing decimal temperature input in preset configurations
Closes: Temperature preset inputs were limited to whole numbers only
* fix: prevent AttributeError crash when TRV event delivers None state (#1926)
* fix: prevent AttributeError when TRV event delivers None state
The guard clause at line 52 used `None in (new_state, old_state,
new_state.attributes)` which evaluates new_state.attributes even
when new_state is None, causing an AttributeError crash.
Replace with short-circuit `or` checks so new_state.attributes is
only accessed after confirming new_state is not None.
Related: #1674, #1648
Tested in: #1925
* chore: clean up test docstrings and fix ruff lint issues
* fix: enforce debounce interval in temperature accept condition (#1931)
The accept condition had a tautological or-branch:
_is_significant and (_interval_ok or _diff_q >= _sig_threshold_q)
Since _is_significant already checks _diff_q >= _sig_threshold_q,
the or-branch was always True when _is_significant was True, making
_interval_ok irrelevant. This bypassed the 5s debounce (and the
600s HomematicIP debounce) for every temperature change.
Fix: Remove the or-branch so "significant" acceptance requires both
_is_significant AND _interval_ok.
Also add a "first_reading" path for when cur_temp is None, ensuring
the very first sensor reading is always accepted regardless of
debounce timing.
Refs: #1474
* fix: enforce cooltemp > target_temp invariant in TRV setpoint sync (#1927)
* fix: enforce cooltemp > target_temp invariant in TRV setpoint sync
The cooler sync logic had two bugs:
1. Two if-conditions (<= and >=) covered ALL cases, making the
second branch always execute after the first (if/if instead of
if/elif).
2. Both branches set cooltemp = target - step, pushing cooltemp
BELOW target_temp. This violates the design invariant that
cooltemp must stay above target_temp (see climate.py:3336).
Fix: Only adjust cooltemp when target_temp rises above it (invariant
violated), and push cooltemp UP to target + step — matching the
enforcement pattern in climate.py and cooler.py.
Related: #1570, #1433, #1588
Tested in: #1925
* chore: clean up test docstrings and fix ruff lint issues
* fix: resolve CI failures (ruff lint, test updates)
- Fix trailing whitespace in config_flow.py
- Apply ruff formatting to files from develop merge
- Update test_returns_early_new_state_none: remove expected
AttributeError since #1926 fixed the guard clause
- Fix test_cooler_sync_pushes_cooltemp_above_target: add proper
test body that exercises trigger_trv_change with cooltemp == target
- Fix test_cooler_sync_always_overwrites docstring
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* fix: allow no_off_system_mode TRVs to recover from OFF state (#1928)
* fix: allow no_off_system_mode TRVs to recover from OFF state
The no_off_system_mode detection logic (line 313) was nested inside
the `bt_hvac_mode is not HVACMode.OFF` guard (line 246). When BT
was in OFF state and the user turned up the TRV dial from min_temp,
the recovery logic was unreachable — BT stayed OFF permanently.
Fix: Bypass the OFF guard for no_off_system_mode devices by adding
`or _is_no_off_device` to the condition. This allows the setpoint
processing block (including clamping, adoption, and mode recovery)
to execute for these devices even when BT is currently OFF.
Closes: #1195
Related: #1760, #1885
Tested in: #1925
* chore: fix ruff lint and formatting from develop rebase
* fix: restore significance threshold to filter sensor noise (#1930)
* fix: restore significance threshold to 0.11 in temperature event handler
The significance threshold was set to 0.0 instead of the intended 0.11,
which made _is_significant always True for any temperature change. This
caused sub-threshold sensor noise to be treated as real changes, and
rendered the accumulation and plateau acceptance paths unreachable dead
code.
Restoring the threshold to 0.11 (as the comment already stated) filters
0.1°C sensor noise and enables the accumulation and plateau heuristics
to work as designed.
Closes #1474
* fix: address review comments on temperature event handling
- Re-check _interval_ok in plateau timer callback so HomematicIP 600s
debounce is not bypassed
- Log EMA update failures at DEBUG instead of bare except pass
- Remove unused accum_since field (written but never read)
- Clarify test docstring for first-run rejection scenario
- Add asyncio_mode = "auto" to pyproject.toml for pytest-asyncio
* refactor: Make compute_mpc() return (output, state) tuple (#1915)
* refactor: Add unified StateManager for runtime state persistence
Introduce a single versioned HA Store per config entry that replaces
the four separate Store files for MPC, PID, TPI, and thermal data.
- Add state_manager.py with RuntimeState, MpcState, PidState, TpiState,
ThermalStats dataclasses and typed serialization/deserialization
- Add StateManager class with dirty-flag tracking, get-or-create
accessors, and async load/save/flush lifecycle
- Add schema migration path (_migrate_v0_to_v1) for future versioning
- Full mypy --strict compliance (zero type: ignore)
- 58 unit tests covering roundtrip, type coercion, edge cases, and
StateManager lifecycle
* refactor: Make MPC computation stateless (caller-owned persistence)
compute_mpc() now accepts an optional `state` parameter and returns
(MpcOutput | None, MpcState) so the caller owns persistence.
When state is None, falls back to module-level dict for backward compat.
* chore: fix ruff lint and formatting from develop rebase
* refactor: Make compute_pid() and compute_tpi() return state in tuple (#1916)
* refactor: Make PID and TPI computation stateless (caller-owned persistence)
compute_pid() now returns (percent, debug, PIDState) and compute_tpi()
returns (TpiOutput | None, TpiState). Both accept an optional state
parameter; when None they fall back to module-level dicts.
* chore: fix ruff lint and formatting from develop rebase
* [TASK] new website (#1932)
* [TASK] cleanup node deps
* [TASK] cleanup pages deploy
* [TASK] add a test deploy for develop
* [TASK] disable a test deploy for develop
* [IMPORTANT] change editable settings swap main trv
* [TASK] remove the obsolete mpc learning sensor
* [FIX] model detection from z2m from 1.7 to 1.8 add ME167 fixes
* [FIX] model detection from z2m from 1.7 to 1.8 add ME167 fixes
* refactor: Wire StateManager into climate.py with async_call_later (#1917)
Replace four separate Store files (MPC, PID, TPI, thermal) with the
unified StateManager introduced in the previous commit.
Key changes:
- async_added_to_hass: init StateManager, run one-time legacy migration,
hydrate module-level controller caches from loaded state
- schedule_save_state: cancelable debounce via async_call_later (replaces
four non-cancelable asyncio.sleep schedulers)
- on_remove: cancel pending timer, sync state, flush to disk
- _sync_controllers_to_state: export module-level caches back to
StateManager before each save
- Inline _migrate_legacy_stores() reads old Store files on first run
* refactor: DRY sensor.py with base classes, strict typing & cleanup (#1944)
* refactor: DRY sensor.py with base class hierarchy
Extract 3 base classes to eliminate massive code duplication:
- _BtSensorBase: shared __init__, async_added_to_hass, _on_climate_update
- _BtMpcSensorBase: shared available property + debug key lookup (was 4x copy-paste)
- _BtSimpleAttributeSensor: shared attribute read + optional rounding (was 3x copy-paste)
- _get_filtered_temp helper: shared fallback logic for external temp sensors
Reduces sensor.py by ~315 lines. Bug fixes now only need 1 change instead of 4.
No behavioral changes — all existing functionality preserved.
* typing: Make sensor.py fully mypy --strict compliant
- Add `from __future__ import annotations` and TYPE_CHECKING block
- Import BetterThermostat type for all bt_climate parameters
- Type all base class methods: __init__, async_added_to_hass, _on_climate_update, _update_state
- Type helper: _get_filtered_temp(bt_climate) -> float | None
- Type EMA sensor: _ema_value: float | None, _update_ema(new_value: float) -> None
- Type all module-level functions: bt_climate, async_add_entities, generic params
- Fix generic type params in globals (dict, set, list, Callable)
- Add assert for _ema_value narrowing after _update_ema call
Result: 0 mypy --strict errors in sensor.py
* cleanup: Remove redundant hasattr checks and extract _get_pid_trvs helper
- Extract duplicated PID TRV detection logic into _get_pid_trvs() helper
(was copy-pasted in _cleanup_pid_number_entities and _cleanup_pid_switch_entities)
- Remove unnecessary hasattr() checks now that bt_climate is typed as BetterThermostat
- Simplify algorithm.value patterns (CalibrationMode always has .value)
- Remove unused total_removed variable
- Simplify had_algorithm_entities / previous_algorithms logic
- Guard real_trvs iteration with truthiness check instead of hasattr
* style: Restore meaningful code comments removed during refactoring
Re-add comments that explain non-obvious behavior:
- "Also update initially" in async_added_to_hass
- HA guideline note in available property docstring
- Why SolarIntensitySensor needs polling
- 0.0-1.0 to % conversion note
- String-to-enum normalization in _get_pid_trvs
* typing: Replace Any with specific types in sensor.py
- Event[Any] → Event[EventStateChangedData]
- data: Any → data: object (value unused)
- Remove Any import
* test: Add comprehensive test suite for sensor platform (#1943)
* refactor: DRY sensor.py with base class hierarchy
Extract 3 base classes to eliminate massive code duplication:
- _BtSensorBase: shared __init__, async_added_to_hass, _on_climate_update
- _BtMpcSensorBase: shared available property + debug key lookup (was 4x copy-paste)
- _BtSimpleAttributeSensor: shared attribute read + optional rounding (was 3x copy-paste)
- _get_filtered_temp helper: shared fallback logic for external temp sensors
Reduces sensor.py by ~315 lines. Bug fixes now only need 1 change instead of 4.
No behavioral changes — all existing functionality preserved.
* typing: Make sensor.py fully mypy --strict compliant
- Add `from __future__ import annotations` and TYPE_CHECKING block
- Import BetterThermostat type for all bt_climate parameters
- Type all base class methods: __init__, async_added_to_hass, _on_climate_update, _update_state
- Type helper: _get_filtered_temp(bt_climate) -> float | None
- Type EMA sensor: _ema_value: float | None, _update_ema(new_value: float) -> None
- Type all module-level functions: bt_climate, async_add_entities, generic params
- Fix generic type params in globals (dict, set, list, Callable)
- Add assert for _ema_value narrowing after _update_ema call
Result: 0 mypy --strict errors in sensor.py
* cleanup: Remove redundant hasattr checks and extract _get_pid_trvs helper
- Extract duplicated PID TRV detection logic into _get_pid_trvs() helper
(was copy-pasted in _cleanup_pid_number_entities and _cleanup_pid_switch_entities)
- Remove unnecessary hasattr() checks now that bt_climate is typed as BetterThermostat
- Simplify algorithm.value patterns (CalibrationMode always has .value)
- Remove unused total_removed variable
- Simplify had_algorithm_entities / previous_algorithms logic
- Guard real_trvs iteration with truthiness check instead of hasattr
* style: Restore meaningful code comments removed during refactoring
Re-add comments that explain non-obvious behavior:
- "Also update initially" in async_added_to_hass
- HA guideline note in available property docstring
- Why SolarIntensitySensor needs polling
- 0.0-1.0 to % conversion note
- String-to-enum normalization in _get_pid_trvs
* typing: Replace Any with specific types in sensor.py
- Event[Any] → Event[EventStateChangedData]
- data: Any → data: object (value unused)
- Remove Any import
* test: Add comprehensive test suite for sensor platform
108 tests covering all 10 sensor classes, setup/teardown, algorithm
detection, and entity cleanup functions in sensor.py.
1 test FAILS, documenting a bug:
- real_trvs=None crashes MPC sensor _update_state with AttributeError
(hasattr check passes but .items() called on None)
107 tests pass, covering:
- External temp EMA sensor (state, fallback, invalid values)
- 1h EMA sensor (math correctness, convergence, edge cases)
- Simple attribute sensors (TempSlope, HeatingPower, HeatLoss)
- MPC sensor availability (4 sensors x 5 conditions)
- MPC sensor state from calibration_balance debug data
- Solar intensity sensor (percent conversion, exceptions)
- _get_active_algorithms (enum conversion, invalid modes)
- _setup_algorithm_sensors (MPC creation, filtering, tracking)
- async_setup_entry (no climate guard, core sensor count)
- async_unload_entry (dispatcher cleanup, tracking dicts)
- Stale algorithm entity cleanup (removal, partial, exceptions)
- Preset/PID/switch entity cleanup (removal, tracking, failures)
- Edge cases (NaN, inf, negative solar, list vs dict)
* test: Update sensor tests for base class hierarchy
Adapt test imports and add tests for the new base classes:
- _BtSensorBase: inheritance checks, __init__ verification
- _BtMpcSensorBase: inheritance checks for MPC sensors
- _BtSimpleAttributeSensor: rounding and attribute read logic
- _get_filtered_temp: fallback helper tests
122 tests total (121 pass, 1 expected fail for known real_trvs=None bug)
* test: Update tests for hasattr removal in sensor base classes
Adapt tests that used `del bt.attr` to simulate missing attributes.
Since bt_climate is now typed as BetterThermostat (all attributes always
present), use None/False values instead of deleting attributes.
* fix: Catch TypeError in HomematicIP detection loop (#1942)
The `except KeyError` handler in the HomematicIP TRV detection loop
(temperature.py:209) does not catch TypeError, which occurs when:
- `self.all_trvs` is None (TypeError: 'NoneType' not iterable)
- `trv["advanced"]` is None (TypeError: 'NoneType' not subscriptable)
Widen the exception handler to `except (KeyError, TypeError)` so that
malformed TRV data degrades gracefully to the default 5s debounce
instead of crashing the temperature event handler.
* fix: resolve cooler setpoint from each state independently (#1940)
The _main_key for reading the cooling setpoint was determined from
old_state.attributes only, then applied to both old and new state.
If the cooler entity switches between "temperature" and
"target_temp_high" attributes across events, the new setpoint was
silently lost (convert_to_float("None") → None → no adoption).
Fix: extract a helper that checks both keys per state independently.
* fix: clamp heat target to min_temp in cooler setpoint sync (#1939)
When the cooler setpoint is clamped to min_temp, the heat-target sync
(line 107) pushed bt_target_temp to cooltemp - step, which could go
below bt_min_temp. Also, a zero-valued step made heat == cool,
breaking the heat < cool invariant.
Fix: use max(step, 0.5) to guarantee separation, and clamp the
result to bt_min_temp.
* fix: guard against hass.states.get() returning None in TRV event handler (#1937)
Add a None check after `hass.states.get(entity_id)` in
`trigger_trv_change()`. Without this guard, any of 9 subsequent
accesses to `_org_trv_state.attributes` / `.state` crash with
`AttributeError` when the entity is temporarily missing from the
state registry (e.g. during removal or unavailability).
Follows the same early-return-with-debug-log pattern already used
for the new_state/old_state guard clause above.
* [TASK] new model fixes ME167 ME167, edit TRVZB for translation key use, fix calibration, add blueprints
* [TASK] new model fixes ME167 ME167, edit TRVZB for translation key use, fix calibration, add blueprints
* [TASK] ruff fix
* [TASK] ruff fix
* [TASK] fix broken state
* refactor: Extract v0 store migration into dedicated module (#1918)
Move the inline _migrate_legacy_stores() method from climate.py to
utils/migrate_v0_stores.py with full test coverage (33 tests).
The module reads the four legacy Store files (MPC, PID, TPI, thermal),
filters entries by entity prefix, and imports them into the unified
StateManager. Runs once on startup when the unified store is empty.
Remove Store import from climate.py (no longer needed directly).
* test: add child_lock None mode propagation test for TRV events (#1925)
* test: Add edge-case tests for temperature event handler (#1941)
* test: Add baseline tests for climate.py (Phase 0) (#1946)
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)
* refactor: Extract thermal learning into utils/thermal_learning.py (#1947)
* test: Add baseline tests for climate.py (Phase 0)
Add 63 regression tests covering the 6 most important methods in
climate.py before refactoring begins. Uses the unbound-method pattern
with MagicMo…
KartoffelToby
added a commit
that referenced
this pull request
Jun 3, 2026
* pid percent rounding
* mpc loss fix
* mpc loss fix
* trvzb valve maintenance interval
* fix: ignore TRV target temp changes during active communication (#1882)
* fix: ignore TRV target temp changes during active communication
When BT sends commands to a TRV, it sets `ignore_trv_states = True` to
prevent the TRV's response from being misinterpreted. However, in the
temperature change handler in events/trv.py, this flag was not checked.
This caused TRV responses (or internal TRV logic like Tado scheduling)
to override `bt_target_temp` even while BT was actively communicating
with the TRV.
This fix adds the missing check for `ignore_trv_states` in the condition
that decides whether to adopt a temperature change from the TRV.
Fixes #1733
* test: add tests for ignore_trv_states flag in temperature change logic
Tests verify that temperature changes from TRVs are correctly blocked
when ignore_trv_states is True during active BT communication.
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* fix: change HEAT to HEAT_MODE if not in available modes (#1880)
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* fix: don't update cooler state if no change is needed (#1879)
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* fix: translate HEAT_COOL mode on TRVs when AC entity is connected (#1878)
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* [TASK] fix lint
* [TASK] fix lint
* [TASK] fix lint
* [TASK] fix test
* Fix issue with translated entities use translation_key (#1896)
* [TASK] make sure the auto find entites are work with multilang
* [TASK] fix test
* feature/dynamic entity cleanup (#1897)
* [BUMP] version
* docs: Add AI development credits to feature request
- Clearly document that this feature was implemented with Claude AI assistance
- Credit AI collaboration for codebase analysis, architecture design, and implementation
- Highlight comprehensive nature of AI-assisted development process
- Provide transparency about AI involvement in the feature development
* fix markdown errors
* fix markdown errors
* fix: Resolve merge conflict in sensor.py - Integrate MpcKaSensor with dynamic entity management
- Integrated BetterThermostatMpcKaSensor from master branch
- Preserved complete dynamic entity management system from feature branch
- Updated MPC entity tracking to include new Ka sensor (5 total sensors)
- Removed redundant has_mpc logic in favor of universal algorithm detection
- All MPC entities (including Ka) now properly managed by cleanup system
- Seamless algorithm switching with automatic entity lifecycle management
Breaking: None
Compatibility: Full backward compatibility maintained
Testing: MPC algorithm diagnostics enhanced with thermal insulation monitoring
Resolves merge conflict between feature/dynamic-entity-cleanup and master
Addresses CodeRabbit merge conflict detection in sensor.py
* fix: Resolve merge conflict in sensor.py - Integrate MpcKaSensor with dynamic entity management
- Integrated BetterThermostatMpcKaSensor from master branch
- Preserved complete dynamic entity management system from feature branch
- Updated MPC entity tracking to include new Ka sensor (5 total sensors)
- Removed redundant has_mpc logic in favor of universal algorithm detection
- All MPC entities (including Ka) now properly managed by cleanup system
- Seamless algorithm switching with automatic entity lifecycle management
Breaking: None
Compatibility: Full backward compatibility maintained
Testing: MPC algorithm diagnostics enhanced with thermal insulation monitoring
Resolves merge conflict between feature/dynamic-entity-cleanup and master
Addresses CodeRabbit merge conflict detection in sensor.py
* fix: Address CodeRabbit review issues - Fix signal format, memory leaks, and grammar
- Fix signal format inconsistency: Use entry_id instead of entity_id in climate.py
- Fix memory leak: Store and use dispatcher unsubscribe functions properly
- Add _DISPATCHER_UNSUBSCRIBES tracking for proper cleanup on unload
- Fix grammar: Change 'Can be aggressive initially' to 'PID can be aggressive initially'
Issues resolved:
- Major: Signal format alignment between climate.py and sensor.py
- Major: Dispatcher callbacks now properly unsubscribed to prevent accumulation
- Minor: Grammar improvement in PID controller documentation
All CodeRabbit requested changes have been addressed.
* fix: Address CodeRabbit review issues - Fix signal format, memory leaks, and grammar
- Fix signal format inconsistency: Use entry_id instead of entity_id in climate.py
- Fix memory leak: Store and use dispatcher unsubscribe functions properly
- Add _DISPATCHER_UNSUBSCRIBES tracking for proper cleanup on unload
- Fix grammar: Change 'Can be aggressive initially' to 'PID can be aggressive initially'
Issues resolved:
- Major: Signal format alignment between climate.py and sensor.py
- Major: Dispatcher callbacks now properly unsubscribed to prevent accumulation
- Minor: Grammar improvement in PID controller documentation
All CodeRabbit requested changes have been addressed.
* fix: Resolve CodeRabbit review issues - Add missing MpcKaSensor class and fix signal handling
- Add missing BetterThermostatMpcKaSensor class implementation in sensor.py
* Fixes critical NameError when MPC calibration mode is active
* Monitors MPC thermal insulation coefficient (Ka parameter)
* Follows established pattern of other MPC sensors (Gain, Loss)
- Fix signal format in climate.py _signal_config_change method
* Change self._entry_id to self._config_entry_id (correct attribute)
* Aligns with signal listener expectations in sensor.py
* Ensures proper config change propagation to entity cleanup handlers
- Add .vscode/settings.json to .gitignore to exclude from public repo
Resolves all critical and major issues identified by CodeRabbit in PR #1887
* chore: Remove .vscode/settings.json from Git tracking
- File should remain local only per .gitignore configuration
* fix: Resolve critical async/await and data structure issues in sensor.py
- Fix async_get_entity_registry call: add 'await' keyword
* Was being called as sync function but returns a coroutine
* This prevented proper entity registry initialization
- Change bt_climate.all_trvs to bt_climate.real_trvs
* all_trvs doesn't exist in the codebase
* real_trvs is the correct dict of loaded TRV instances
* Fixes AttributeError in _get_active_algorithms
- Update loop to handle dict.items() from real_trvs
* Changed from 'for trv in all_trvs' to 'for trv_id, trv in real_trvs.items()'
* Ensures proper unpacking of dictionary data structure
These fixes ensure:
- Entity registry operations complete successfully
- Algorithm detection works with actual TRV data
- No AttributeError at runtime when checking calibration modes
* fix: Use sync entity registry accessor in sensor cleanup
* fix: Log invalid calibration modes in sensor algorithm detection
* fix: Align MPC Ka sensor unit with changelog
* fix: Keep algorithm tracking when entity cleanup is partial
* feat: Add comprehensive cleanup for unused preset, PID number and switch entities
- Add automatic cleanup for unused preset input numbers (requested by @wtom)
- Extend cleanup system to PID number entities (Kp, Ki, Kd parameters)
- Add cleanup for PID auto-tune switch entities
- Implement entity tracking in number.py and switch.py platforms
- Enhance sensor.py with unified cleanup functions for all dynamic entities
- Add proper unload functions for tracking cleanup
- Maintain compatibility with existing algorithm sensor cleanup
Addresses code owner feedback in PR #1887 for complete entity management.
Co-authored-by: Claude AI <claude@anthropic.com>
* fix: Run entity cleanup on all config changes, not just algorithm changes
Addresses CodeRabbit feedback: preset cleanup was only triggered when algorithms
changed, causing stale preset number entities to remain when users only disabled
presets without changing calibration modes.
- Move _cleanup_unused_number_entities() outside algorithm-change conditional
- Ensure preset/PID number cleanup runs on every configuration change signal
- Maintains performance by only triggering on actual config changes via dispatcher
Fixes: https://github.com/KartoffelToby/better_thermostat/pull/1887#discussion_r1734567890
* fix: Use consistent TRV data source and key access in PID cleanup functions
- Both PID number and switch cleanup now use bt_climate.real_trvs (dict)
- Both use CONF_CALIBRATION_MODE constant instead of hardcoded string
- Ensures cleanup functions work on same TRV dataset consistently
- Addresses CodeRabbit feedback about inconsistent data sources
* docs: Add clarifying comment about TRV data source consistency
- Explicitly note that PID number cleanup uses same data source as switch cleanup
- Helps clarify the consistency fix from previous commit
- Addresses any remaining questions about TRV data source alignment
* fix: Normalize CalibrationMode values before PID comparison
- Add proper string-to-enum conversion in PID calibration checks
- Handle string-configured modes correctly in sensor.py cleanup functions
- Apply same normalization fix to switch.py PID creation logic
- Use CONF_CALIBRATION_MODE constant consistently in switch.py
- Prevents comparison issues between raw strings and enum values
* fix: Guard against None preset_modes in cleanup function
- Add null safety check for bt_climate.preset_modes before set conversion
- Use empty list as fallback when preset_modes is None or falsy
- Prevents TypeError when presets are unsupported by the climate entity
- Maintains existing logic for removing 'none' preset from cleanup scope
* feat: Extend switch cleanup to handle child lock switches
- Add cleanup logic for orphaned child lock switches when TRVs are removed
- Parse both _pid_auto_tune and _child_lock suffixes from switch unique IDs
- Remove child lock switches for TRVs that no longer exist in real_trvs
- Use same entity registry removal logic for consistent cleanup behavior
- Update logging to reflect cleanup of both switch types
- Addresses CodeRabbit feedback for comprehensive switch entity management
* feat: Add native SELECT entity support for HomeMatic temperature offset
- Extend find_local_calibration_entity() to discover SELECT temperature_offset entities
- Add fallback search for SELECT entities when device-based search fails
- Modify get_current_offset() to strip 'k' suffix from SELECT values (e.g., '1.5k' -> 1.5)
- Update set_offset() to detect entity domain and use appropriate service:
* SELECT entities: Use select.select_option with 'k' suffix formatting
* NUMBER entities: Continue using number.set_value (backward compatibility)
- Enhance get_min_offset() and get_max_offset() to parse options list for SELECT entities
- Support HomeMatic HM-CC-RT-DN thermostats via homematicip_local integration
- Eliminates need for template NUMBER entity workarounds
- Maintains full backward compatibility with existing NUMBER entity setups
- Resolves GitHub issue #1888
* Update custom_components/better_thermostat/adapters/generic.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* fix: Improve SELECT entity handling robustness in set_offset
- Make domain derivation null-safe using entity_id when entity_state is None
- Add comprehensive option validation for SELECT entities
- Handle missing entity_state gracefully by using empty options list
- Improve closest option matching with robust error handling
- Parse each option individually to handle malformed options
- Only call select_option with validated options from the entity
- Fallback to original option_value if parsing fails
- Enhances reliability for HomeMatic SELECT temperature offset entities
* docs: Fix markdown violations and add HomeMatic SELECT documentation
- Fix formatting in CHANGELOG_PRESET_NUMBER_CLEANUP.md and CLEANUP_REVIEW_SUMMARY.md
- Improve README.md structure and heading hierarchy
- Add comprehensive HomeMatic IP/CCU SELECT entity documentation
- Document temperature_offset activation steps for HomeMatic users
- Fix markdown compliance across all documentation files
* Update docs/Configuration/configuration.md
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Update CHANGELOG_PRESET_NUMBER_CLEANUP.md
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* fix: Correct spelling and formatting in CLEANUP_REVIEW_SUMMARY.md
- Fix German compound hyphenation: Debug/Info Logging → Debug-/Info-Logging
- Remove trailing whitespace after 'activity' in test scenarios
- Address CodeRabbit spelling/typography issues
* docs: Translate CLEANUP_REVIEW_SUMMARY.md to English for consistency
- Translate German content to English to match rest of codebase
- Maintain consistent language across all documentation files
- Keep technical details and formatting intact
- Ensure professional documentation standards
* chore: rollback to stable state and update manifest/gitignore
- Rollback to commit 743ae5a before version 1.8.0-dev update
- Revert defensive NoneType fixes that caused integration instability
- Update manifest.json with correct version
- Adjust .gitignore for development workflow
* fix: Use lazy formatting in watcher logger call
Replace f-string with lazy %s formatting in DEBUG logger call to avoid
eager evaluation cost when log level is disabled. Aligns with all other
log calls in the module.
* remove individual unused files
* fix: improve exception handling specificity in climate.py
- Replace broad 'except Exception' catches with specific exception types
- Improves debuggability and prevents false exception masking
- Addresses 12 instances across multiple operation contexts:
* Model/device detection: AttributeError, TypeError
* Storage init/load (PID/MPC/TPI/Thermal): FileNotFoundError, PermissionError, RuntimeError
* External temperature operations: OSError, RuntimeError, AttributeError, TypeError
* Async task creation: RuntimeError
* EMA initialization: ValueError, TypeError, ImportError
* Heat loss calculations: ValueError, TypeError, KeyError
Changes maintain backward compatibility while improving error handling granularity.
* style: fix lazy logging and trailing whitespace
- Replace f-string with lazy % formatting in logger calls
- Remove trailing whitespace across 11 files
- Improves logging performance (lazy evaluation)
- No functional changes
Files: generic.py, config_flow.py, cooler.py, window.py, BTH-RM.py, TV02-Zigbee.py, number.py, sensor.py, switch.py, helpers.py, test_off_temperature_attribute.py
* fix: address CodeRabbit cleanup and add pylint config
* style: ignore unused-argument warnings in pylintrc
Callback handlers in Home Assistant often have unused arguments (e.g., event
parameter in dispatcher callbacks). This is intentional and not an error.
* style: fix unused variables in sensor.py and mpc.py
- Replace unused 'trv_id' loop variables with underscore (5 occurrences)
- Remove unused 'solar_gain_factor' variable assignment
* style: fix line length violations
* style: remove trailing whitespace and reorganize imports
* config: ignore import-outside-toplevel warnings (C0415) for tests
* config: ignore protected-access warnings (W0212)
* config: restrict pylint to custom_components directory only
* config: restrict linting to custom_components only (pylint + pyright)
* config: improve pyright ignore patterns for tmp and other directories
* fix: remove pyright include to preserve package structure
* fix: add missing __init__.py to adapters package
* fix: add missing __init__.py files to all Python packages
* fix: resolve pylint/pylance errors in helpers.py
- Add @staticmethod decorators to rounding class methods
- Fix round_by_step to properly default to rounding.nearest
- Add None checks for config_entry_id before API calls
- Add None check for entry in get_trv_intigration
- Fix device_id handling in find_valve_entity
* style: fix pylint/pylance infos and improve code quality
- Remove unnecessary pass statements in exception classes
- Initialize heating_cycles and heating_power_normalized in __init__ to prevent lazy initialization
- Replace inefficient .keys() iterations with direct dict iteration (Python 3 best practice)
- Change .items() iterations for direct access to dict values (performance improvement)
- Add debug logging to asyncio.CancelledError handler for better observability
- Move import statement to proper location after module aliases
- Add module docstrings to simulator files (pid_simulator, mpc_simulator)
All changes address pylint/pylance info messages while maintaining backwards compatibility and code stability.
* fix: add None-safety guards for thermal_store and attribute access
- Add explicit None checks before float() conversions for ATTR_TEMPERATURE, ATTR_STATE_HEATING_POWER, and ATTR_STATE_HEAT_LOSS
- Guard _thermal_store.async_load() calls to satisfy Pyright type checking
- Store .get() values in intermediate variables to improve type inference
- Resolves 6 Pyright None-safety errors while maintaining defensive programming patterns
* fix: add humidity state safety check and optimize type annotations
- Add None check for humidity_state.state access at line 862 (fixes .state type error)
- Import cached_property from functools for proper type compatibility
- Change device_info decorator from @property to @cached_property to match Entity base class signature
- Improves defensive programming for Home Assistant state access patterns
All None-safety errors now resolved (7/7).
* chore: update .gitignore with comprehensive Python and development patterns
- Add Python bytecode patterns (*.pyc, *.pyo, *.py[cod])
- Add Python package build artifacts (dist/, build/, *.egg-info/)
- Expand virtual environment patterns (ENV/, env/, .env)
- Add testing artifacts (.pytest_cache/, .tox/, htmlcov/)
- Add IDE patterns (sublime, swap files)
- Add OS-specific files (Thumbs.db, Desktop.ini)
- Add Home Assistant database and log files
- Add temporary and backup file patterns
- Organize with category comments for better maintainability
- Keep manifest and spec files (needed for packaging)
* fix: restore broken target temperature state recovery
- Revert redundant double None-check in ATTR_TEMPERATURE restore logic
- The additional safety check in else branch caused state restore to fail
- This led to 'Undefined target temperature, falling back to 7.0' and
HVAC mode defaulting to OFF, preventing MPC calibration from running
- Restores original working logic that was modified in previous commits
Fixes Better Thermostat state restore regression causing MPC sensors
to remain unknown after restart due to disabled calibration.
* fix: add missing exception handling for heating power state restore
- Add try/catch (TypeError, ValueError) around float() conversion for ATTR_STATE_HEATING_POWER
- Matches existing exception handling pattern used for heat loss restore
- Prevents startup crashes when corrupted heating power values are stored
- Ensures consistent error handling across all state restore operations
Fixes potential ValueError crash during startup sequence when non-numeric
heating power values (e.g. 'unknown') are encountered in saved state.
* [MERGE]
* [MERGE]
---------
Co-authored-by: Mulatz Florian (IT OS IC WA GO) <florian.mulatz@infineon.com>
Co-authored-by: Florian Mulatz <florian@mulatz.at>
Co-authored-by: Florian Mulatz <33655308+florianmulatz@users.noreply.github.com>
Co-authored-by: Claude AI <claude@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Fix aggressive calibration not applied in hysteresis band (#1823)
* Fix aggressive calibration not applied in hysteresis band
Change the condition for applying aggressive calibration from checking
hvac_action == HEATING to checking cur_temp < bt_target_temp.
The previous condition failed in the hysteresis band (between target-tolerance
and target) because:
- When temperature is in this band and system was previously IDLE
- The hysteresis logic keeps hvac_action as IDLE
- So aggressive calibration adjustment was not applied
- Result: TRV temperatures stayed "too close" to target
The fix ensures aggressive calibration is applied whenever the room actually
needs heating (cur_temp < target), regardless of the hysteresis-driven
hvac_action state.
Closes #1790
* Fix aggressive calibration by skipping tolerance delay
The real issue was that aggressive calibration mode was subject to the
same tolerance delay as DEFAULT mode. When hvac_action is IDLE and
heating should start, the tolerance delay (tolerance * 2.0) was added
to the calibration, effectively making aggressive mode LESS aggressive
when starting to heat.
Fix: Skip the tolerance delay for AGGRESIVE_CALIBRATION mode while
keeping the -2.5 offset behavior unchanged (only applies when HEATING).
This addresses the root cause identified in wtom's review comment.
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* [TASK] implement code suggestions
* mpc claude changes
* trv compensation
* [TASK] code fixes
* fix: implement proper HA availability pattern for MPC sensors (#1899)
- Add @property available() to all MPC sensor classes
- Return False when thermostat is OFF, window is open, or climate not available
- This ensures sensors show 'unavailable' instead of 'unknown' per HA best practices
- Affected sensors: virtual_temperature, mpc_gain, mpc_loss, mpc_insulation_ka
Resolves issue where MPC sensors showed 'unknown' state inappropriately.
* fix: Prevent complex number crash in heating_power_valve_position (#1870)
* fix: Prevent complex number crash in heating_power_valve_position
Fixes crash when heating_power_valve_position is called with negative
temperature difference (cur_temp > target_temp).
This occurs in rare TRV override edge case:
1. System heating normally (cur_temp < target_temp)
2. Fast temperature rise (sun, external heat)
3. TRV reports "heating" with delay
4. _compute_hvac_action() overrides to HEATING
5. Function called with negative temp_diff
6. Formula produces complex number: (-x) ** 0.946
7. TypeError on comparison
Changes:
- Add guard for temp_diff <= 0 at function start
- Return 0.0 (no heating needed) when room warmer than target
- Add debug logging for this edge case
- Add comprehensive tests (extracted from #1868)
Related to #1868 (test coverage PR)
Affects: HEATING_POWER_CALIBRATION mode only
* test: Remove meta-comments from test docstrings
* style: fix ruff formatting in test_helpers_valve_position
* refactor: Clean up test docstrings and remove meta-communication
Remove bug-hunting language ("FIXED:", "BUG:", severity ratings),
try/except pytest.fail patterns, and unused pytest import. Rename
test methods to describe expected behavior instead of referencing bugs.
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* fix: Add boost mode support for available TRVs (#1873)
* fix: Add boost mode support for available TRVs
**Problem:**
Boost mode logic (lines 296-313) only existed in the unavailable TRV path.
When a TRV was available (normal case), boost mode had no effect.
**Root Cause:**
Code duplication between unavailable (lines 262-582) and available (584-829)
TRV paths led to missing boost mode implementation in the available path.
**Fix:**
Added boost mode logic to available TRV path (after line 611):
1. Set temperature to max_temp when boost mode is active (lines 613-619)
2. Set valve to 100% when boost mode is active (lines 625-632)
This mirrors the existing boost mode logic from the unavailable path.
**Related Issues:**
- Fixes #1817 - Support native operation modes (Boost/Vacation/Profiles)
- Users reported boost mode not working - this explains why it only worked
when TRV was unavailable (which is the exceptional case, not the norm)
**Testing:**
Added test_boost_mode_bug.py with tests documenting the bug and verifying
that boost mode now works for both unavailable and available TRVs.
* test: Clean up test and add missing bt_hvac_mode
- Remove meta-comments from docstrings (BUG, After the fix)
- Add missing bt_hvac_mode attribute to mock
- Rename test to describe behavior, not bug
- Clean up assertions
* fix: Move set_valve call outside elif to fix boost mode
- Critical bug: set_valve was inside elif block, never executed for boost mode
- Moved set_valve logic after if/elif/else so it executes for both boost and calibration
- Fixed in both unavailable TRV path (lines 307-397) and available TRV path (lines 625-710)
- Updated tests with proper mocks (adapter, model_quirks, cooler_entity_id, etc.)
- All 3 boost mode tests now pass
* test: Fix import ordering in test_boost_mode_bug.py
* fix: Reset valve when safety overrides force HVAC OFF in boost mode
When boost mode sets valve to 100% but safety checks (window open or
call_for_heat=False) force HVAC mode to OFF, the valve was left at 100%.
This created conflicting commands that could cause unpredictable TRV behavior.
Now explicitly reset valve to 0% when safety overrides activate during boost.
Addresses CodeRabbit review feedback on PR #1873.
* refactor: Extract valve control helpers and fix safety override
- Add _is_boost_heating_active() to check boost condition
- Add _get_valve_control() to determine valve settings from boost or calibration
- Add _apply_valve_control() to apply valve settings to TRV
- Add _reset_valve_on_safety_override() to reset valve when HVAC forced OFF
- Remove redundant cooler section from unavailable TRV path (handled by control_cooler)
- Reduce code duplication between unavailable and available TRV paths
- Add tests for safety override scenarios (window open, no heat call)
- Rename test file from test_boost_mode_bug.py to test_boost_mode.py
* fix: Remove unused solar_gain_factor variable
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* fix: Handle TypeError in check_float for None and invalid types (#1869)
* fix: Handle TypeError in check_float for None and invalid types
Fixes bug where check_float() raised TypeError instead of returning
False when passed None or invalid types (list, dict, etc.).
The function only caught ValueError but not TypeError, which is raised
when float() is called with incompatible types.
Changes:
- Add TypeError to exception handling in check_float()
- Add comprehensive tests (extracted from #1868)
Related to #1868 (test coverage PR)
Fixes bug #2 documented in #1868
Tests:
- test_returns_false_for_none ✅
- test_returns_false_for_invalid_types ✅
- All 6 tests pass
* test: Remove unused pytest import
* refactor: Remove meta-communication from test module docstring
* style: Fix ruff format
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* [TASK] cleanup code
* max open percentage as input number
* lint
* fix: import_mpc_state_map drops string and int fields during state restore (#1904)
* fix: import_mpc_state_map drops string and int fields during state restore
import_mpc_state_map uses float() as default coercion for all fields
not explicitly handled. This silently drops string fields like
trv_profile ("threshold", "linear", etc.) because float("threshold")
raises ValueError, which is caught and the field is skipped.
Additionally, profile_samples (int) was not listed in the int-coercion
branch, causing it to be coerced to float instead of int.
Changes:
- Add trv_profile to str() coercion branch
- Add profile_samples to int() coercion branch
- Add tests covering round-trip for all field types
* refactor: Use match-case for type coercion in import_mpc_state_map
* fix: Add type annotation for coerced variable in match-case
mypy infers the type from the first case arm only. Restore the
explicit union annotation so all branches type-check correctly.
* fix: add missing match-cases for bool/int fields and extend round-trip tests
Address Copilot review feedback:
- Add match-cases for regime_boost_active (bool) and
consecutive_insufficient_heat (int) so they survive import with
correct types instead of being coerced to float.
- Add round-trip tests for perf_curve (dict), regime_boost_active,
and consecutive_insufficient_heat.
- Include all new fields in the full_state_round_trip test.
- Clean up double import of mpc module.
---------
Co-authored-by: Wolfgang Tom <wtom@users.noreply.github.com>
* fix: Detect regime change when all prediction errors are identical nonzero (#1905)
* fix: Detect regime change when all prediction errors are identical nonzero
When all recent prediction errors are identical and nonzero (std=0),
_detect_regime_change returned False because the t-test division by
std is undefined. However, identical nonzero errors represent a
perfectly consistent bias that should trigger a regime change.
Return mean_error != 0 instead of False when std_error == 0.
* test: replace flaky random.gauss test with fixed deterministic series
Address Copilot review: test_high_variance_masks_small_bias used
random.gauss which made it non-deterministic and only asserted the
return type. Replace with a fixed error series (mean ≈ 0.01,
std ≈ 0.39) and assert the expected False result.
* fix: Extend lock coverage to prevent race conditions in parallel TRV control (#1875)
* fix: Skip all control operations for unavailable TRVs
After PR #1813, the code logged "skipping control" for unavailable TRVs but
still executed all operations (Lines 271-582), including:
- convert_outbound_states()
- set_valve()
- set_hvac_mode()
- set_offset()
- set_temperature()
This was wasteful and could cause issues when these operations failed.
Changed to return True immediately after detecting unavailable TRV without
executing any operations. The TRV state change event will trigger a new control
cycle when it becomes available.
Removed 311 lines of unnecessary code execution for unavailable TRVs.
* fix: Extend lock coverage to prevent race conditions in parallel TRV control
## Problem
When control_queue() runs control_trv() in parallel for multiple TRVs
(using asyncio.gather), race conditions occur because _temp_lock only
protected lines 240-257, leaving critical operations unprotected:
- set_valve() (line 358)
- set_hvac_mode() (line 430)
- set_offset() (line 492)
- set_temperature() (line 509)
This caused issues with grouped TRVs where both TRVs access shared state
(self.cur_temp, self.bt_target_temp, etc.) simultaneously.
## Fix
Extended the lock scope to protect ALL critical operations from line 240
to line 518 (end of function). Now when multiple TRVs are controlled in
parallel:
- TRV1 acquires lock
- TRV1 completes ALL operations
- Lock is released
- TRV2 acquires lock
- TRV2 completes ALL operations
Operations are now serialized, preventing race conditions.
## Test Coverage
Added test_race_condition_lock_coverage.py with 3 tests:
1. test_parallel_trv_control_no_race_condition: Verifies no interleaving
2. test_shared_state_corruption_in_parallel_execution: Checks state integrity
3. test_lock_protects_critical_sections: Confirms lock is held during operations
Test results show operations now run sequentially instead of interleaving.
## Related Issues
Fixes:
- #1839: Automation stuck on climate.set_temperature (2 grouped TRVs)
- #1145: Grouped TRVs automatically turn off
- #875: Grouped thermostats turned to 30°C
- #1733: BT changes Target Temp with no input from user
- #1849: Random target temperature changes
All these issues share the same root cause: race conditions when controlling
multiple TRVs in parallel without proper synchronization.
* test: Fail test when race condition is detected
- Replace early return with AssertionError
- Test now properly fails if operations interleave
- With fix in place, no race condition should occur
* fix: Remove unused imports and fix import ordering
- Remove unused PRESET_BOOST import from controlling.py
- Fix import ordering in test_race_condition_lock_coverage.py
- Fix import ordering in test_unavailable_trv_no_operations.py
- Remove unused HVACMode import
Note: test_shared_state_corruption_in_parallel_execution has a pre-existing failure (KeyError: 'adapter')
* docs: Add missing docstrings to controlling.py
Add docstrings to control_trv, control_queue, control_cooler,
TaskManager class, handle_window_open, check_system_mode,
and check_target_temperature functions.
* fix: Address CodeRabbit review comments
- Add early guard for heater_entity_id before accessing real_trvs
- Remove duplicate calculate_heating_power() call and _trv fetch
- Move asyncio.sleep(3) outside lock scope to allow parallel TRV control
- Add None/unavailable check in check_target_temperature()
- Update tests to force state mismatch for critical operation coverage
- Patch all delegate functions in tests to avoid retry delays
* docs: Add docstrings to test helper functions
* refactor: Remove meta-communication from tests and code comments
Remove bug tracking references (Bug #5), GitHub issue numbers, PR
references, and process-oriented language from test docstrings and
inline comments. Keep technical descriptions of what is being tested.
* style: Fix ruff format
* refactor: clean up meta-communication comments in controlling.py
Remove change-history comments, commented-out code, developer names,
and process-oriented descriptions. Keep only comments that describe
what the code does.
* test: Comprehensive MPC controller test coverage (#1903)
* test: add comprehensive MPC controller tests (105 tests, 3 expected failures)
Covers helper functions, state persistence, core control logic,
adaptive learning, virtual temperature, regime change detection,
TRV profile detection, post-processing, perf curve sampling,
forced calibration, stale state detection, sibling seeding,
and edge cases.
3 tests fail and expose real bugs:
- import_mpc_state_map drops string fields (trv_profile)
- _detect_regime_change returns False for constant nonzero errors
* test: adapt MPC tests to develop, add group-key + distribute tests
- Rebase on develop, fix time() vs monotonic() mismatch
- Rewrite virtual-temp tests for Kalman-based API (no invalid kwargs)
- Remove test_std_zero_returns_false (tested buggy behavior as correct)
- Add 4 tests for build_mpc_group_key (multi-TRV group key)
- Add 10 tests for distribute_valve_percent (TRV compensation)
- 115 passed, 3 failed (3 documented bugs in production code)
* test: add 34 tests covering Kalman filter, analytical solver, and learning paths
Cover develop's new MPC subsystems:
- Kalman filter: P init/predict/update, gain K at high/low P, physics predict
- Analytical solver: zero-gain fallback, cost debug, negative error
- max_opening_pct clamping
- gain_learn_count >= 2 guard for warm_low_u loss learning
- Gain recovery path (over-correction upward)
- High-u steady-state gain learning
- ka_est outdoor-dependent dynamic loss initialization and update
- Residual rate-limiting boundaries (300s, 3600s)
- Big-change force-open bypass (hold-time bypass for large increases only)
- Deque eviction (maxlen=20)
- Stale state anchor reset
- Target change 0.05K boundary
- distribute_valve_percent negative input
- Solar gain initialization
- Integration accumulation and window-open reset
Total: 152 tests (149 pass, 3 expected failures from documented bugs).
* test: address Copilot review feedback
- Fix docstring: replace "time is mocked via monkeypatch" with accurate
description (real time with large deltas)
- Fix error message in test_valve_monotonically_increases_with_error:
use actual temps array instead of incorrect formula
- Clean up double import of mpc module
* test: Add comprehensive unit tests for helpers.py (#1868)
* test: Add comprehensive unit tests for helpers.py functions
Add 87 new unit tests for helper functions in helpers.py to improve
test coverage and document existing bugs:
New test files:
- test_helpers_normalize.py (34 tests): Tests for calibration mode,
HVAC mode normalization, and float conversion functions
- test_helpers_rounding.py (22 tests): Tests for rounding helpers
and float validation with floating-point precision edge cases
- test_helpers_valve_position.py (16 tests): Tests for heating power
valve position calculation including edge case bug documentation
- test_helpers_mode_remap.py (16 tests): Tests for HVAC mode remapping
between Better Thermostat and TRVs with quirk handling
Bugs found and fixed in separate PRs:
1. [MEDIUM-HIGH] heating_power_valve_position crashes with complex
numbers in TRV override edge case (negative temp_diff)
→ Fixed in PR #1870
2. [HIGH] check_float raises TypeError for None/invalid types instead
of returning False (missing TypeError catch)
→ Fixed in PR #1869
3. [NOT A BUG] convert_to_float rounds values < 0.005 to 0 - this is
by design (PR #1805 fixed #1792, #1789, #1785). The 0.01 step
rounding preserves sensor precision while preventing artifacts.
Tests updated to reflect this is expected behavior.
4. [MEDIUM] rounding enum methods have incorrect epsilon behavior
→ Documented in tests, not critical
5. [LOW] Floating-point precision issues in round_by_step
→ Documented in tests, cosmetic only
Related PRs:
- #1869: Fix check_float TypeError (extracted tests from this PR)
- #1870: Fix heating_power_valve_position edge case (extracted tests from this PR)
- #1867: Fix rounding enum (already merged)
All tests pass. Critical bugs (#1, #2) are fixed in separate PRs.
* test: Remove meta-comments from rounding tests
* test: Remove remaining meta-comments from test_helpers_rounding.py
* test: Remove meta-comments and fix ruff linter issues
- Renamed test_bug_* to test_* (removed 'bug' prefix)
- Fixed docstrings to describe behavior without meta-language
- Removed unused 'result' variables
- All ruff checks pass (except pre-existing mpc.py issue)
- All tests pass
* test: fix tests to assert correct expected behavior instead of current bugs
check_float should return False for None/invalid types, not raise TypeError.
heating_power_valve_position should return 0.0 for negative temp diffs, not
produce complex numbers. Tests now fail until the corresponding fixes land.
* refactor: Remove meta-communication from test docstrings
Remove PR references, "BY DESIGN" labels, issue numbers, and
bug-hunting try/except patterns. Replace with objective descriptions
of expected behavior. Remove unused pytest import.
* style: Fix ruff format issues
* fix: Improve type hints for mypy compliance (#1867)
* fix: Improve type hints and add mypy CI enforcement
This commit fixes all mypy type errors and enforces type checking in CI
to prevent future type regressions.
- **pid.py**: Created PIDDebugInfo TypedDict for proper debug dict typing (19 dict-item errors)
- **mpc.py**: Added None check for state.loss_est before division (1 arg-type error)
- **model_fixes/default.py**: Define STATE_LOCKED/UNLOCKED locally as they don't exist in HA const (2 attr-defined errors)
- **switch.py**: Added explicit list[SwitchEntity] type annotation (1 arg-type error)
- **number.py**: Added explicit list[NumberEntity] type annotation (3 arg-type errors)
- **utils/helpers.py**:
- Changed rounding class from Enum to static methods
- Added Callable type hint for f_rounding parameter (5 errors)
- **utils/weather.py**: Created typed list for valid temperatures before sum() (1 arg-type error)
- **device_trigger.py**: Added proper return type with dict[str, bool] for metadata (1 return-value error)
- **device_condition.py**: Fixed function signatures to use Mapping[str, object] | None (2 return-value errors)
- **config_flow.py**:
- Avoided TypedDict ** expansion by explicitly passing arguments
- Added type annotations for trv_bundle and updated_config (3 errors)
- **utils/helpers.py**:
- `normalize_calibration_mode`: Changed from Any to CalibrationMode | str | None
- `is_calibration_mode`: Changed from Any to CalibrationMode | str | None
- **config_flow.py**:
- `_as_bool`: Changed from Any to bool | str | int | None
- `_duration_dict_to_seconds`: Changed from Any to int | float | dict[str, int] | None
- `_seconds_to_duration_dict`: Changed from Any to int | float | str | None
- `add_entity_selector`: Changed domain from Any to str | list[str]
- **utils/calibration/tpi.py**: `_round_dbg`: Changed from Any to float | int | None
- **utils/calibration/mpc.py**: `_round_for_debug`: Changed from Any to float | int | None
- Added `.github/workflows/mypy.yml` to enforce type checking on push and PRs
- Workflow uses Python 3.13 and uv for dependency management
- All 41 source files now pass mypy type checking
- Verified all mypy errors are resolved (0 errors in 41 files)
- Confirmed existing tests still pass (25 passed, 97 pre-existing errors)
- No new test failures introduced by type hint changes
* fix: Address PR review comments
- Fix mypy workflow to use system-installed mypy directly instead of `uv run mypy`
This ensures mypy can see the packages installed with `uv pip install --system`
- Modernize lock state handling to use LockState enum instead of string constants
This follows Home Assistant's current best practices and improves type safety
* docs: Add docstrings to helper functions in config_flow.py
Add docstrings to resolve(), add_field(), add_entity_selector(),
get_value(), and get_bool() closure functions within _build_user_fields
and _build_advanced_fields.
* fix: Remove mypy CI workflow and fix type annotations for develop compatibility
Remove the separate mypy.yml CI workflow. Fix type annotations in
sensor.py, switch.py, number.py, and config_flow.py to pass mypy
after rebasing on develop's entity-cleanup changes. Use assert
statements to narrow _attr_unique_id types and proper generic types
instead of Any for module-level tracking dicts.
* style: Fix ruff lint and format issues
* fix: Add type annotations and None guards for sensor.py mypy compliance
* [TASK] fix timeout and tolerance bugs (#1909)
* [TASK] fix timeout and tolerance bugs
* [TASK] fix testing and lint
* mpc tolerance - new overshoot penalty instead of eco penalty
* 10% close hold bypass
* lint
* fix: Reduce false degraded-mode warnings during startup (#1907)
* fix: Reduce false degraded-mode warnings during startup
Optional sensors (outdoor, weather, window, humidity) frequently need
seconds to initialise after a Home Assistant reboot. The previous
startup code checked them once, immediately logged WARNING-level
messages, and set degraded_mode=True — even though the sensors came
online moments later and degraded mode self-resolved.
Changes:
- Remove the premature per-sensor WARNING checks and the
"Starting in DEGRADED MODE" message from startup entirely
- Add a retry loop with increasing delays (3/5/10/15/25 s, ~60 s max)
that waits for optional sensors before the first
check_and_update_degraded_mode() call — exits early as soon as all
sensors are available
- Let check_and_update_degraded_mode() be the single source of truth
for degraded mode (it already handles logging, HA issues, and state
transitions correctly)
The room-temperature-sensor fallback (semi-critical) retains its
WARNING because it represents a genuinely degraded state.
Closes #1902
* refactor: Extract await_optional_sensors() and add tests
Extract the startup retry loop into a testable
await_optional_sensors() function in watcher.py with:
- Injectable _sleep parameter for deterministic testing
- Configurable delay sequence (default 3/5/10/15/25 s)
- Final check after last sleep to catch late arrivals
- Returns list of still-pending entity IDs
Add 9 unit tests covering:
- All sensors available immediately (no sleep)
- No optional sensors configured
- Sensor comes online after retry
- All retries exhausted (returns pending sensors)
- Default delays are strictly increasing and sum to ~60 s
- Custom delay overrides
- Partial sensor availability (some online, some not)
- Final check catches sensors that arrive during last sleep
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* [TASK] fix duplicate tests
* test: Add unit tests for utils/controlling.py (#1871)
* test: Add unit tests for utils/controlling.py
Add 5 new test files covering the controlling module (61 tests):
- test_controlling_control_cooler.py — control_cooler()
- test_controlling_control_queue.py — control_queue()
- test_controlling_control_trv.py — control_trv()
- test_controlling_helpers.py — handle_window_open(), check_system_mode(),
check_target_temperature()
- test_controlling_task_manager.py — TaskManager
Tests adapted to current develop API:
- control_cooler now sends set_temperature before set_hvac_mode
- Unavailable TRVs trigger early-return skip (use available state in tests)
* test: Restructure controlling tests into tests/unit/controlling/ package
Move all utils/controlling.py tests into a dedicated package for better
organization. Absorb scattered test files and deduplicate:
- test_control_trv.py: merged from test_boost_mode.py (safety overrides),
test_race_condition_lock_coverage.py, test_grouped_trv_calibration.py,
and test_unavailable_trv_no_operations.py (72 tests total)
- test_control_queue.py: moved (15 tests)
- test_helpers.py: merged with handle_window_open tests from
test_window_no_off_mode.py (16 tests)
- test_control_cooler.py: moved (9 tests)
- test_task_manager.py: moved (5 tests)
Removed test_control_queue_retry.py (documentation-only pass stubs).
Kept test_window_no_off_mode.py for events/trv.py tests only.
* fix: resolve mypy errors in test_mpc_comprehensive.py (#1913)
Replace **kwargs dict-unpacking for MpcParams/MpcInput dataclasses
with explicitly typed function signatures. mypy cannot match
`dict[str, object]` to individual dataclass field types.
- _default_params: list all MpcParams fields with correct types,
use _UNSET sentinel + dataclasses.replace for optional overrides
- _inp: list all MpcInput fields directly, construct dataclass
without dict-unpacking
* refactor: Add unified StateManager for runtime state persistence (#1914)
* refactor: Add unified StateManager for runtime state persistence
Introduce a single versioned HA Store per config entry that replaces
the four separate Store files for MPC, PID, TPI, and thermal data.
- Add state_manager.py with RuntimeState, MpcState, PidState, TpiState,
ThermalStats dataclasses and typed serialization/deserialization
- Add StateManager class with dirty-flag tracking, get-or-create
accessors, and async load/save/flush lifecycle
- Add schema migration path (_migrate_v0_to_v1) for future versioning
- Full mypy --strict compliance (zero type: ignore)
- 58 unit tests covering roundtrip, type coercion, edge cases, and
StateManager lifecycle
* fix: harden _deserialize against corrupted store data
- Guard mpc/pid/tpi sections with isinstance(…, Mapping) before
calling .items(), so non-dict payloads don't crash deserialization
- Wrap float() conversion for thermal stats in try/except so
non-numeric values yield None instead of raising
* Fix: Enable decimal temperature input for preset number entities (#1922)
* fix: implement proper HA availability pattern for MPC sensors
- Add @property available() to all MPC sensor classes
- Return False when thermostat is OFF, window is open, or climate not available
- This ensures sensors show 'unavailable' instead of 'unknown' per HA best practices
- Affected sensors: virtual_temperature, mpc_gain, mpc_loss, mpc_insulation_ka
Resolves issue where MPC sensors showed 'unknown' state inappropriately.
* Fix: Enable decimal input for temperature presets
- Fixed preset temperature number entities to support decimal values (e.g., 23.5°C)
- Updated TEMP_STEP_SELECTOR to include backwards compatibility for existing "0.0" configs
- Modified target_temperature_step handling to treat both "" and "0.0" as auto mode (None)
- Set default step of 0.1°C for preset number entities when auto mode is enabled
- Resolves validation errors preventing decimal temperature input in preset configurations
Closes: Temperature preset inputs were limited to whole numbers only
* fix: prevent AttributeError crash when TRV event delivers None state (#1926)
* fix: prevent AttributeError when TRV event delivers None state
The guard clause at line 52 used `None in (new_state, old_state,
new_state.attributes)` which evaluates new_state.attributes even
when new_state is None, causing an AttributeError crash.
Replace with short-circuit `or` checks so new_state.attributes is
only accessed after confirming new_state is not None.
Related: #1674, #1648
Tested in: #1925
* chore: clean up test docstrings and fix ruff lint issues
* fix: enforce debounce interval in temperature accept condition (#1931)
The accept condition had a tautological or-branch:
_is_significant and (_interval_ok or _diff_q >= _sig_threshold_q)
Since _is_significant already checks _diff_q >= _sig_threshold_q,
the or-branch was always True when _is_significant was True, making
_interval_ok irrelevant. This bypassed the 5s debounce (and the
600s HomematicIP debounce) for every temperature change.
Fix: Remove the or-branch so "significant" acceptance requires both
_is_significant AND _interval_ok.
Also add a "first_reading" path for when cur_temp is None, ensuring
the very first sensor reading is always accepted regardless of
debounce timing.
Refs: #1474
* fix: enforce cooltemp > target_temp invariant in TRV setpoint sync (#1927)
* fix: enforce cooltemp > target_temp invariant in TRV setpoint sync
The cooler sync logic had two bugs:
1. Two if-conditions (<= and >=) covered ALL cases, making the
second branch always execute after the first (if/if instead of
if/elif).
2. Both branches set cooltemp = target - step, pushing cooltemp
BELOW target_temp. This violates the design invariant that
cooltemp must stay above target_temp (see climate.py:3336).
Fix: Only adjust cooltemp when target_temp rises above it (invariant
violated), and push cooltemp UP to target + step — matching the
enforcement pattern in climate.py and cooler.py.
Related: #1570, #1433, #1588
Tested in: #1925
* chore: clean up test docstrings and fix ruff lint issues
* fix: resolve CI failures (ruff lint, test updates)
- Fix trailing whitespace in config_flow.py
- Apply ruff formatting to files from develop merge
- Update test_returns_early_new_state_none: remove expected
AttributeError since #1926 fixed the guard clause
- Fix test_cooler_sync_pushes_cooltemp_above_target: add proper
test body that exercises trigger_trv_change with cooltemp == target
- Fix test_cooler_sync_always_overwrites docstring
---------
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* fix: allow no_off_system_mode TRVs to recover from OFF state (#1928)
* fix: allow no_off_system_mode TRVs to recover from OFF state
The no_off_system_mode detection logic (line 313) was nested inside
the `bt_hvac_mode is not HVACMode.OFF` guard (line 246). When BT
was in OFF state and the user turned up the TRV dial from min_temp,
the recovery logic was unreachable — BT stayed OFF permanently.
Fix: Bypass the OFF guard for no_off_system_mode devices by adding
`or _is_no_off_device` to the condition. This allows the setpoint
processing block (including clamping, adoption, and mode recovery)
to execute for these devices even when BT is currently OFF.
Closes: #1195
Related: #1760, #1885
Tested in: #1925
* chore: fix ruff lint and formatting from develop rebase
* fix: restore significance threshold to filter sensor noise (#1930)
* fix: restore significance threshold to 0.11 in temperature event handler
The significance threshold was set to 0.0 instead of the intended 0.11,
which made _is_significant always True for any temperature change. This
caused sub-threshold sensor noise to be treated as real changes, and
rendered the accumulation and plateau acceptance paths unreachable dead
code.
Restoring the threshold to 0.11 (as the comment already stated) filters
0.1°C sensor noise and enables the accumulation and plateau heuristics
to work as designed.
Closes #1474
* fix: address review comments on temperature event handling
- Re-check _interval_ok in plateau timer callback so HomematicIP 600s
debounce is not bypassed
- Log EMA update failures at DEBUG instead of bare except pass
- Remove unused accum_since field (written but never read)
- Clarify test docstring for first-run rejection scenario
- Add asyncio_mode = "auto" to pyproject.toml for pytest-asyncio
* refactor: Make compute_mpc() return (output, state) tuple (#1915)
* refactor: Add unified StateManager for runtime state persistence
Introduce a single versioned HA Store per config entry that replaces
the four separate Store files for MPC, PID, TPI, and thermal data.
- Add state_manager.py with RuntimeState, MpcState, PidState, TpiState,
ThermalStats dataclasses and typed serialization/deserialization
- Add StateManager class with dirty-flag tracking, get-or-create
accessors, and async load/save/flush lifecycle
- Add schema migration path (_migrate_v0_to_v1) for future versioning
- Full mypy --strict compliance (zero type: ignore)
- 58 unit tests covering roundtrip, type coercion, edge cases, and
StateManager lifecycle
* refactor: Make MPC computation stateless (caller-owned persistence)
compute_mpc() now accepts an optional `state` parameter and returns
(MpcOutput | None, MpcState) so the caller owns persistence.
When state is None, falls back to module-level dict for backward compat.
* chore: fix ruff lint and formatting from develop rebase
* refactor: Make compute_pid() and compute_tpi() return state in tuple (#1916)
* refactor: Make PID and TPI computation stateless (caller-owned persistence)
compute_pid() now returns (percent, debug, PIDState) and compute_tpi()
returns (TpiOutput | None, TpiState). Both accept an optional state
parameter; when None they fall back to module-level dicts.
* chore: fix ruff lint and formatting from develop rebase
* [TASK] new website (#1932)
* [TASK] cleanup node deps
* [TASK] cleanup pages deploy
* [TASK] add a test deploy for develop
* [TASK] disable a test deploy for develop
* [IMPORTANT] change editable settings swap main trv
* [TASK] remove the obsolete mpc learning sensor
* [FIX] model detection from z2m from 1.7 to 1.8 add ME167 fixes
* [FIX] model detection from z2m from 1.7 to 1.8 add ME167 fixes
* refactor: Wire StateManager into climate.py with async_call_later (#1917)
Replace four separate Store files (MPC, PID, TPI, thermal) with the
unified StateManager introduced in the previous commit.
Key changes:
- async_added_to_hass: init StateManager, run one-time legacy migration,
hydrate module-level controller caches from loaded state
- schedule_save_state: cancelable debounce via async_call_later (replaces
four non-cancelable asyncio.sleep schedulers)
- on_remove: cancel pending timer, sync state, flush to disk
- _sync_controllers_to_state: export module-level caches back to
StateManager before each save
- Inline _migrate_legacy_stores() reads old Store files on first run
* refactor: DRY sensor.py with base classes, strict typing & cleanup (#1944)
* refactor: DRY sensor.py with base class hierarchy
Extract 3 base classes to eliminate massive code duplication:
- _BtSensorBase: shared __init__, async_added_to_hass, _on_climate_update
- _BtMpcSensorBase: shared available property + debug key lookup (was 4x copy-paste)
- _BtSimpleAttributeSensor: shared attribute read + optional rounding (was 3x copy-paste)
- _get_filtered_temp helper: shared fallback logic for external temp sensors
Reduces sensor.py by ~315 lines. Bug fixes now only need 1 change instead of 4.
No behavioral changes — all existing functionality preserved.
* typing: Make sensor.py fully mypy --strict compliant
- Add `from __future__ import annotations` and TYPE_CHECKING block
- Import BetterThermostat type for all bt_climate parameters
- Type all base class methods: __init__, async_added_to_hass, _on_climate_update, _update_state
- Type helper: _get_filtered_temp(bt_climate) -> float | None
- Type EMA sensor: _ema_value: float | None, _update_ema(new_value: float) -> None
- Type all module-level functions: bt_climate, async_add_entities, generic params
- Fix generic type params in globals (dict, set, list, Callable)
- Add assert for _ema_value narrowing after _update_ema call
Result: 0 mypy --strict errors in sensor.py
* cleanup: Remove redundant hasattr checks and extract _get_pid_trvs helper
- Extract duplicated PID TRV detection logic into _get_pid_trvs() helper
(was copy-pasted in _cleanup_pid_number_entities and _cleanup_pid_switch_entities)
- Remove unnecessary hasattr() checks now that bt_climate is typed as BetterThermostat
- Simplify algorithm.value patterns (CalibrationMode always has .value)
- Remove unused total_removed variable
- Simplify had_algorithm_entities / previous_algorithms logic
- Guard real_trvs iteration with truthiness check instead of hasattr
* style: Restore meaningful code comments removed during refactoring
Re-add comments that explain non-obvious behavior:
- "Also update initially" in async_added_to_hass
- HA guideline note in available property docstring
- Why SolarIntensitySensor needs polling
- 0.0-1.0 to % conversion note
- String-to-enum normalization in _get_pid_trvs
* typing: Replace Any with specific types in sensor.py
- Event[Any] → Event[EventStateChangedData]
- data: Any → data: object (value unused)
- Remove Any import
* test: Add comprehensive test suite for sensor platform (#1943)
* refactor: DRY sensor.py with base class hierarchy
Extract 3 base classes to eliminate massive code duplication:
- _BtSensorBase: shared __init__, async_added_to_hass, _on_climate_update
- _BtMpcSensorBase: shared available property + debug key lookup (was 4x copy-paste)
- _BtSimpleAttributeSensor: shared attribute read + optional rounding (was 3x copy-paste)
- _get_filtered_temp helper: shared fallback logic for external temp sensors
Reduces sensor.py by ~315 lines. Bug fixes now only need 1 change instead of 4.
No behavioral changes — all existing functionality preserved.
* typing: Make sensor.py fully mypy --strict compliant
- Add `from __future__ import annotations` and TYPE_CHECKING block
- Import BetterThermostat type for all bt_climate parameters
- Type all base class methods: __init__, async_added_to_hass, _on_climate_update, _update_state
- Type helper: _get_filtered_temp(bt_climate) -> float | None
- Type EMA sensor: _ema_value: float | None, _update_ema(new_value: float) -> None
- Type all module-level functions: bt_climate, async_add_entities, generic params
- Fix generic type params in globals (dict, set, list, Callable)
- Add assert for _ema_value narrowing after _update_ema call
Result: 0 mypy --strict errors in sensor.py
* cleanup: Remove redundant hasattr checks and extract _get_pid_trvs helper
- Extract duplicated PID TRV detection logic into _get_pid_trvs() helper
(was copy-pasted in _cleanup_pid_number_entities and _cleanup_pid_switch_entities)
- Remove unnecessary hasattr() checks now that bt_climate is typed as BetterThermostat
- Simplify algorithm.value patterns (CalibrationMode always has .value)
- Remove unused total_removed variable
- Simplify had_algorithm_entities / previous_algorithms logic
- Guard real_trvs iteration with truthiness check instead of hasattr
* style: Restore meaningful code comments removed during refactoring
Re-add comments that explain non-obvious behavior:
- "Also update initially" in async_added_to_hass
- HA guideline note in available property docstring
- Why SolarIntensitySensor needs polling
- 0.0-1.0 to % conversion note
- String-to-enum normalization in _get_pid_trvs
* typing: Replace Any with specific types in sensor.py
- Event[Any] → Event[EventStateChangedData]
- data: Any → data: object (value unused)
- Remove Any import
* test: Add comprehensive test suite for sensor platform
108 tests covering all 10 sensor classes, setup/teardown, algorithm
detection, and entity cleanup functions in sensor.py.
1 test FAILS, documenting a bug:
- real_trvs=None crashes MPC sensor _update_state with AttributeError
(hasattr check passes but .items() called on None)
107 tests pass, covering:
- External temp EMA sensor (state, fallback, invalid values)
- 1h EMA sensor (math correctness, convergence, edge cases)
- Simple attribute sensors (TempSlope, HeatingPower, HeatLoss)
- MPC sensor availability (4 sensors x 5 conditions)
- MPC sensor state from calibration_balance debug data
- Solar intensity sensor (percent conversion, exceptions)
- _get_active_algorithms (enum conversion, invalid modes)
- _setup_algorithm_sensors (MPC creation, filtering, tracking)
- async_setup_entry (no climate guard, core sensor count)
- async_unload_entry (dispatcher cleanup, tracking dicts)
- Stale algorithm entity cleanup (removal, partial, exceptions)
- Preset/PID/switch entity cleanup (removal, tracking, failures)
- Edge cases (NaN, inf, negative solar, list vs dict)
* test: Update sensor tests for base class hierarchy
Adapt test imports and add tests for the new base classes:
- _BtSensorBase: inheritance checks, __init__ verification
- _BtMpcSensorBase: inheritance checks for MPC sensors
- _BtSimpleAttributeSensor: rounding and attribute read logic
- _get_filtered_temp: fallback helper tests
122 tests total (121 pass, 1 expected fail for known real_trvs=None bug)
* test: Update tests for hasattr removal in sensor base classes
Adapt tests that used `del bt.attr` to simulate missing attributes.
Since bt_climate is now typed as BetterThermostat (all attributes always
present), use None/False values instead of deleting attributes.
* fix: Catch TypeError in HomematicIP detection loop (#1942)
The `except KeyError` handler in the HomematicIP TRV detection loop
(temperature.py:209) does not catch TypeError, which occurs when:
- `self.all_trvs` is None (TypeError: 'NoneType' not iterable)
- `trv["advanced"]` is None (TypeError: 'NoneType' not subscriptable)
Widen the exception handler to `except (KeyError, TypeError)` so that
malformed TRV data degrades gracefully to the default 5s debounce
instead of crashing the temperature event handler.
* fix: resolve cooler setpoint from each state independently (#1940)
The _main_key for reading the cooling setpoint was determined from
old_state.attributes only, then applied to both old and new state.
If the cooler entity switches between "temperature" and
"target_temp_high" attributes across events, the new setpoint was
silently lost (convert_to_float("None") → None → no adoption).
Fix: extract a helper that checks both keys per state independently.
* fix: clamp heat target to min_temp in cooler setpoint sync (#1939)
When the cooler setpoint is clamped to min_temp, the heat-target sync
(line 107) pushed bt_target_temp to cooltemp - step, which could go
below bt_min_temp. Also, a zero-valued step made heat == cool,
breaking the heat < cool invariant.
Fix: use max(step, 0.5) to guarantee separation, and clamp the
result to bt_min_temp.
* fix: guard against hass.states.get() returning None in TRV event handler (#1937)
Add a None check after `hass.states.get(entity_id)` in
`trigger_trv_change()`. Without this guard, any of 9 subsequent
accesses to `_org_trv_state.attributes` / `.state` crash with
`AttributeError` when the entity is temporarily missing from the
state registry (e.g. during removal or unavailability).
Follows the same early-return-with-debug-log pattern already used
for the new_state/old_state guard clause above.
* [TASK] new model fixes ME167 ME167, edit TRVZB for translation key use, fix calibration, add blueprints
* [TASK] new model fixes ME167 ME167, edit TRVZB for translation key use, fix calibration, add blueprints
* [TASK] ruff fix
* [TASK] ruff fix
* [TASK] fix broken state
* refactor: Extract v0 store migration into dedicated module (#1918)
Move the inline _migrate_legacy_stores() method from climate.py to
utils/migrate_v0_stores.py with full test coverage (33 tests).
The module reads the four legacy Store files (MPC, PID, TPI, thermal),
filters entries by entity prefix, and imports them into the unified
StateManager. Runs once on startup when the unified store is empty.
Remove Store import from climate.py (no longer needed directly).
* test: add child_lock None mode propagation test for TRV events (#1925)
* test: Add edge-case tests for temperature event handler (#1941)
* test: Add baseline tests for climate.py (Phase 0) (#1946)
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)
* refactor: Extract thermal learning into utils/thermal_learning.py (#1947)
* test: Add baseline tests for climate.py (Phase 0)
Add 63 regression tests covering the 6 most important methods in
climate.py before refactoring begins. Uses the unbound-method pattern
with MagicM…
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Problem
After a reboot, optional sensors (outdoor, weather, window, humidity) often need a few seconds to initialise. The startup code checked them immediately and logged WARNING-level messages plus set
degraded_mode=True— even though the sensors came online moments later and degraded mode self-resolved.This caused confusing log entries like:
PR #1826 previously fixed a similar issue in
weather.py(downgraded "no outdoor sensor data found" to debug), but the startup degraded-mode logic inclimate.pywas a separate code path that was not addressed.History
Solution
Remove premature startup checks: The per-sensor WARNING checks and the "Starting in DEGRADED MODE" message are removed entirely from
startup(). They were redundant withcheck_and_update_degraded_mode()which already handles logging, HA issues, and state transitions correctly.Add retry loop with increasing delays: Before the first
check_and_update_degraded_mode()call, a retry loop checks optional sensors with increasing intervals:The loop exits early as soon as all optional sensors are available — no unnecessary waiting.
Room sensor fallback preserved: The room-temperature-sensor fallback (semi-critical, not optional) retains its WARNING because it represents a genuinely degraded state.
Closes #1902
Test plan
check_and_update_degraded_mode()still creates/removes HA issues as expected