Conversation
* 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>
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
Co-authored-by: Tobias Haber <kontakt@t-haber.de>
* [TASK] make sure the auto find entites are work with multilang * [TASK] fix test
* [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: #1887 (comment) * 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>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds SELECT-based calibration support, dispatcher-driven config-change signals, dynamic per-entry algorithm sensors with cleanup/unload hooks, translation_key-based entity discovery, refined valve/control helpers and boost handling, TRV ignore-state short-circuiting, logging hardening, tests, docs, and tooling configs. Changes
Sequence Diagram(s)sequenceDiagram
participant ConfigFlow as ConfigFlow/OptionsFlow
participant Dispatcher as Dispatcher
participant SensorMgr as sensor.py
participant NumberMgr as number/switch managers
ConfigFlow->>ConfigFlow: _check_calibration_changes(old, new)
alt algorithms changed
ConfigFlow->>Dispatcher: dispatcher_send(bt_config_changed_{entry_id})
Dispatcher->>SensorMgr: bt_config_changed_{entry_id}
SensorMgr->>SensorMgr: _handle_dynamic_entity_update(entry_id)
SensorMgr->>NumberMgr: create/remove algorithm sensors, numbers, switches
NumberMgr->>NumberMgr: _cleanup_stale_algorithm_entities(entry_id)
else no change
Note over ConfigFlow: no dispatch
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
* 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>
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
custom_components/better_thermostat/utils/controlling.py (1)
386-448:⚠️ Potential issue | 🟠 MajorRemove duplicate cooler control from
control_trv().Lines 386-448 duplicate cooler control logic that already executes at line 94 via the centralized
control_cooler()call. Sincecontrol_cooler()is invoked before anycontrol_trv()tasks and includes optimizations (state checking, conditional command sending), this block should be removed to avoid sending duplicate commands to the cooler.tmp/mpc_simulator.py (1)
6-20:⚠️ Potential issue | 🟡 Minor
sys.path.appendis placed after the imports it's meant to enable.Line 20 appends the workspace root to
sys.path, but thecustom_componentsimport on lines 12–17 executes first. If this script is ever run from a directory where the module isn't already discoverable, the import will fail before the path fix takes effect.Proposed fix
+import os +import sys + +# Add workspace root to sys.path (must precede local imports) +sys.path.append(os.getcwd()) + from dataclasses import dataclass import logging -import os -import sys from unittest.mock import patch from custom_components.better_thermostat.utils.calibration.mpc import ( _MPC_STATES, MpcInput, MpcParams, compute_mpc, ) - -# Add workspace root to sys.path -sys.path.append(os.getcwd())custom_components/better_thermostat/utils/retry.py (2)
44-48:⚠️ Potential issue | 🟡 MinorOff-by-one in
entity_idextraction from positional args.The comment says "self and entity_id are first two args" (indices 0 and 1), but the guard requires
len(args) > 2(i.e., at least 3 args). This meansentity_idwon't be extracted from calls with exactly 2 positional args (self,entity_id). Should belen(args) > 1.Proposed fix
entity_id = kwargs.get("entity_id", None) - if ( - entity_id is None and len(args) > 2 - ): # Assuming self and entity_id are first two args + if ( + entity_id is None and len(args) > 1 + ): # Assuming self is args[0] and entity_id is args[1] entity_id = args[1]
89-92:⚠️ Potential issue | 🟡 Minor
_LOGGER.exceptionoutside an activeexceptblock won't capture a traceback.At line 91, execution has left the
exceptscope, sosys.exc_info()returns(None, None, None). Using_LOGGER.exceptionhere logs the message but appends a uselessNoneType: Noneinstead of the actual traceback. Consider switching to_LOGGER.erroror passingexc_info=last_exception.Proposed fix
- log_message = f"{log_prefix}{func.__name__} failed after {retries + 1} attempts: {last_exception}{entity_suffix}" - _LOGGER.exception(log_message) + log_message = f"{log_prefix}{func.__name__} failed after {retries + 1} attempts: {last_exception}{entity_suffix}" + _LOGGER.error(log_message, exc_info=last_exception)custom_components/better_thermostat/events/trv.py (1)
301-309:⚠️ Potential issue | 🟡 MinorPre-existing: both cooltemp guards always resolve to the same assignment.
The two conditions on lines 302 and 306 (
<=and>=) together cover every possible relationship betweenbt_target_tempandbt_target_cooltemp, sobt_target_cooltempis unconditionally set tobt_target_temp - bt_target_temp_step. If the intent was to only adjust when the heat/cool targets cross, the second branch likely needs a different assignment (e.g.,bt_target_temp + step).custom_components/better_thermostat/translations/en.json (1)
106-106:⚠️ Potential issue | 🟡 MinorTypo: "Aggresive" → "Aggressive" in the options calibration_mode description.
Also, the MPC description here says "hydraulic balance algorithm" while the config step (line 41) says "predictive controller" — these should be consistent. The
strings.jsonfile (line 39) uses "Aggressive" (correct) and a different MPC phrasing as well.
🤖 Fix all issues with AI agents
In `@custom_components/better_thermostat/events/cooler.py`:
- Around line 29-36: The check builds a tuple including new_state.attributes
before testing for None, which can raise when new_state is None; change the
guard in the Cooler event handler to first check new_state and old_state (e.g.,
if new_state is None or old_state is None) and only then check
new_state.attributes (e.g., if new_state.attributes is None) before logging and
returning; update the condition around the _LOGGER.debug/return so you never
access new_state.attributes unless new_state is not None.
In `@custom_components/better_thermostat/model_fixes/BTH-RM.py`:
- Around line 49-52: entity_reg.async_get(entity_id) can return None so change
the handling around entry in the function that reads entry.capabilities (lookup
using entity_reg and async_get) to guard against None before accessing
attributes: check if entry is None and if so set hvac_modes = [] (or an
appropriate default) or early-return, otherwise read
entry.capabilities.get("hvac_modes", []); apply the same None-guard pattern to
the analogous code in BTH-RM230Z.py to avoid AttributeError when the entity is
not registered.
In `@custom_components/better_thermostat/number.py`:
- Around line 64-71: The code directly compares
advanced.get(CONF_CALIBRATION_MODE) to CalibrationMode.PID_CALIBRATION which
fails when the stored value is a string; mirror the normalization used in
switch.py by first normalizing/mapping the raw value to the CalibrationMode enum
(e.g. read calib = advanced.get(CONF_CALIBRATION_MODE), if calib is a str
attempt to map it to CalibrationMode via CalibrationMode(calib) or
CalibrationMode[calib.upper()] in a try/except) and then compare that enum to
CalibrationMode.PID_CALIBRATION before creating BetterThermostatPIDNumber
instances (refer to CONF_CALIBRATION_MODE, CalibrationMode,
BetterThermostatPIDNumber and trv_conf to locate the change).
In `@custom_components/better_thermostat/sensor.py`:
- Around line 160-182: The code is recreating sensors for all active algorithms
even if already registered, causing duplicate unique_id collisions; modify the
logic so that only algorithms that are newly added are passed to or created by
_setup_algorithm_sensors (or add a guard inside _setup_algorithm_sensors) by
using algorithms_added (and filtering out any algorithm keys present in
_ACTIVE_ALGORITHM_ENTITIES) before creating sensor objects for bt_climate;
ensure _setup_algorithm_sensors either accepts a list/set of
algorithms_to_create or checks _ACTIVE_ALGORITHM_ENTITIES and skips creation for
algorithms already tracked so no duplicate entities with the same unique_id are
instantiated.
In `@custom_components/better_thermostat/utils/controlling.py`:
- Around line 177-192: The current check uses "if current_temp != desired_temp"
which treats current_temp=None as always different; change it to explicitly
handle unknown current_temp by sending the command when current_temp is None or
different (e.g., "if current_temp is None or current_temp != desired_temp"), and
add a debug log via _LOGGER.debug when current_temp is None to state that
temperature is unknown for self.cooler_entity_id so the set_temperature command
(the async_call to "climate.set_temperature" using self.cooler_entity_id and
desired_temp with context self.context) is being sent.
- Around line 167-175: The conditional that sets desired_mode uses
self.cur_temp, self.bt_target_cooltemp, self.tolerance (and compares to
self.bt_target_temp) without None-safety; add an early guard before that block
to check if any of self.cur_temp, self.bt_target_cooltemp, self.tolerance, or
self.bt_target_temp is None and if so return or set desired_mode = HVACMode.OFF
(i.e. bail out early) to avoid TypeError; update the method containing this
conditional to perform this None-check before evaluating the comparison.
In `@docs/Configuration/configuration.md`:
- Line 82: The docs callout uses a single device-specific parameter name
TEMPERATURE_OFFSET:MASTER@HM-CC-RT-DN which may not apply to all HomeMatic
models; update the text to clarify that parameter paths can vary by model (e.g.,
HmIP-eTRV, HM-CC-RT-DN-BoM) and instruct users to check their device-specific
parameter name in the device manual or interface, and give
TEMPERATURE_OFFSET:MASTER@HM-CC-RT-DN as an example rather than the only valid
key.
In `@pyproject.toml`:
- Around line 64-83: Ruff's target-version setting ("py313") and Pyright's
pythonVersion ("3.12") are inconsistent; update one so both match (e.g., set
Ruff's target-version to "py312" or Pyright's pythonVersion to "3.13") to ensure
consistent linting/type-checking; edit the Ruff config entry that defines
target-version and/or the [tool.pyright] pythonVersion entry so they use the
same Python version string.
🧹 Nitpick comments (12)
tmp/pid_simulator.py (1)
17-18:sys.path.appendis executed after the import it's meant to support.The
custom_componentsimport on lines 11–15 runs beforesys.path.append(os.getcwd())on line 18. This works only if the working directory is already onsys.path(e.g., viaPYTHONPATHor IDE config). If this script is ever invoked standalone from a different directory, the import will fail. Consider moving the path manipulation above thecustom_componentsimport.Proposed reorder
+import os +import sys + +# Add workspace root to path so custom_components is importable +sys.path.append(os.getcwd()) + """PID controller simulator for testing the PID calibration logic. ... """ import logging -import os -import sys from unittest.mock import patch from custom_components.better_thermostat.utils.calibration.pid import ( PIDParams, compute_pid, reset_pid_state, ) - -# Add workspace root to path -sys.path.append(os.getcwd())tmp/mpc_simulator.py (1)
1-4: Consider whethertmp/mpc_simulator.pyshould be committed to the repository.This file lives under a
tmp/directory, which conventionally holds throwaway/scratch files. If it's meant as a development utility, consider moving it to a proper location (e.g.,scripts/ortools/) or addingtmp/to.gitignoreto avoid cluttering the repo..pylintrc (2)
27-29:max-line-lengthinconsistent with Ruff.Ruff enforces
line-length = 88(pyproject.toml line 3) while pylint allows 150. If Ruff is the primary linter/formatter, consider aligning pylint's limit or disabling its line-length check entirely (disable=C0301) to avoid confusion.
18-25: DisablingW0718while also listing overgeneral exceptions is redundant.
W0718(broad-exception-caught) is disabled in[MESSAGES CONTROL], but[EXCEPTIONS]still definesovergeneral-exceptions. The disable takes precedence, so the[EXCEPTIONS]block is effectively dead config for this warning. This is fine functionally but may confuse future maintainers.custom_components/better_thermostat/utils/calibration/mpc.py (1)
1373-1401: Remove unusedsolar_gain_estfield from state, export list, and initialization.The solar gain logic is fully disabled (lines 1375–1400 all commented out), yet
solar_gain_estcontinues to be initialized at line 914–915 and exported/imported via_STATE_EXPORT_FIELDS(line 161). This dead field wastes cycles on initialization and persists across state save/load cycles without ever being consumed. Remove the field definition (line 121), the export entry (line 161), and the initialization block (lines 914–915).custom_components/better_thermostat/events/trv.py (2)
459-460: Bare_LOGGER.error(e)loses the traceback.In
convert_outbound_states, the catch-all logs only the exception message. Use_LOGGER.error("...: %s", e, exc_info=True)or_LOGGER.exception(...)to preserve the stack trace for debugging.Proposed fix
except Exception as e: - _LOGGER.error(e) + _LOGGER.exception( + "better_thermostat %s: convert_outbound_states failed for %s: %s", + self.device_name, + entity_id, + e, + ) return None
201-202: Silentexcept Exception: passswallows errors in hvac_action / valve_position handling.If an unexpected error occurs while caching
hvac_actionorvalve_position, it will be silently ignored, making debugging difficult. Consider at minimum logging at debug level.Proposed fix
- except Exception: - pass + except Exception: + _LOGGER.debug( + "better_thermostat %s: failed to update hvac_action/valve_position for %s", + self.device_name, + entity_id, + exc_info=True, + ).gitignore (1)
78-82: Duplicate entries:*.swpand*~already appear in the IDEs section (lines 42, 44).Lines 81–82 duplicate rules from lines 42 and 44. While git handles duplicates gracefully, removing them keeps the file tidy.
Proposed fix
# Temporary files *.tmp *.bak -*.swp -*~custom_components/better_thermostat/model_fixes/TRVZB.py (1)
25-31: Tightened exception handling in_cancel_pending_valve_bump— minor note.
task.cancel()itself doesn't raiseCancelledError(it only schedules cancellation on the task). TheCancelledErrorcatch here is harmless but effectively dead code.RuntimeError(e.g., closed event loop) is the realistic exception to guard against.custom_components/better_thermostat/utils/helpers.py (1)
697-729:find_local_calibration_entitydoesn't break after finding the first translation_key match — last match wins silently.In the first pass (lines 699-710), if multiple entities on the same device have a matching
translation_key, the loop keeps overwritingcalibration_entitywithout breaking. The same applies in the string-match fallback (lines 714-729). This means if two calibration-like entities exist (e.g., one for local temp calibration and one for external offset), the last one iterated wins non-deterministically.Consider adding a
breakafter finding the first match in each pass, or implementing a scoring/priority mechanism similar tofind_valve_entity.♻️ Proposed fix — break after first match in each pass
if tk and tk in _CALIBRATION_TRANSLATION_KEYS: _LOGGER.debug( "better thermostat: Found local calibration entity %s for %s (translation_key=%s)", entity.entity_id, entity_id, tk, ) calibration_entity = entity.entity_id + break # Second pass: fallback to string matching on unique_id / entity_id / original_name if calibration_entity is None: for entity in entity_entries: if entity.device_id != reg_entity.device_id: continue descriptor = f"{entity.unique_id} {entity.entity_id} {getattr(entity, 'original_name', '') or ''}".lower() if ( "temperature_calibration" in descriptor or "temperature_offset" in descriptor or "temperatur_offset" in descriptor or "local_temperature" in descriptor ): _LOGGER.debug( "better thermostat: Found local calibration entity %s for %s (string match)", entity.entity_id, entity_id, ) calibration_entity = entity.entity_id + breaktests/test_helpers_valve.py (1)
453-495: Test may not catch the "last match wins" bug in production code.This test (line 491) passes
[ent_str, ent_tk]— sinceent_strhastranslation_key=None, it won't match in the first pass, soent_tk(withtranslation_key="temperature_calibration") will be the only first-pass match andcalibration_entitywill be set correctly. However, if two entities both had valid translation keys, the test wouldn't catch the non-deterministic "last wins" behavior noted in thefind_local_calibration_entityreview. Consider adding a test case with two entities that both have valid translation keys to verify which one is selected.tests/unit/test_trv_ignore_states.py (1)
64-88: Tests duplicate production condition logic instead of exercising the actual function — reduces regression coverage.All four tests inline-reconstruct the temperature adoption condition instead of calling the real
trigger_trv_change()function fromevents/trv.py. The condition at lines 73–86 (and repeated in subsequent tests) mirrors the production check atevents/trv.py:271–282exactly. If the production condition changes—e.g., a clause is reordered, a new check is added, or a field is renamed—these tests will still pass while silently diverging from the actual behavior.Consider extracting the condition into a helper method in the
BetterThermostatclass and having tests call it directly (passing the necessary temperature and TRV state parameters). This would provide genuine regression coverage.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
custom_components/better_thermostat/calibration.py (2)
92-120:⚠️ Potential issue | 🟠 MajorCopy-paste bug: cloud coverage check duplicated, UV index block shadowed.
Lines 102–110 are an exact duplicate of lines 92–100 (both check
cloud_coveragewith identical logic). Since the first block returns on a match, the duplicate on lines 102–110 is unreachable dead code. The real UV index check on lines 112–120 is fine but the duplicate block above it should be removed.🐛 Proposed fix: remove duplicate block
except (ValueError, TypeError): pass - # 2. UV Index (0-10+) -> Higher is better - for source in sources: - cc = _get_val(source, "cloud_coverage") - if cc is not None: - try: - # 0% clouds = 1.0 intensity, 100% clouds = 0.0 intensity - return max(0.0, min(1.0, (100.0 - float(cc)) / 100.0)) - except (ValueError, TypeError): - pass - # 2. UV Index (0-10+) -> Higher is better for source in sources: uv = _get_val(source, "uv_index")
575-578:⚠️ Potential issue | 🟠 MajorAggressive calibration condition mismatch with commit message — code is intentional but documentation is misleading.
The commit message describes two changes, but they contradict each other and the code. The first part claims: "Change the condition for applying aggressive calibration from checking
hvac_action == HEATINGto checkingcur_temp < bt_target_temp." The second part states: "Skip the tolerance delay for AGGRESIVE_CALIBRATION mode while keeping the -2.5 offset behavior unchanged (only applies when HEATING)."The actual code on line 576 still checks
self.attr_hvac_action == HVACAction.HEATING, notcur_temp < bt_target_temp. The promised condition change was never applied. The real fix (evident from the code diff) was skipping the tolerance delay elsewhere, not changing this condition.The code is intentional—the -2.5 offset applies only when actively HEATING—but the commit message is inaccurate about what was changed. Update the commit message to reflect that the fix skipped the tolerance delay, not the heating condition.
🤖 Fix all issues with AI agents
In `@tests/test_aggressive_calibration.py`:
- Around line 37-51: The test incorrectly computes _skip_post_adjustments
compared to production: align the test with calculate_calibration_local() by
including CalibrationMode.DEFAULT in the tuple used to set
_skip_post_adjustments (so DEFAULT skips post-adjustments), or alternatively
update the failing assertions in tests test_default_idle_has_delay,
test_setpoint_default_has_tolerance_delay, and
test_scenario_temperature_drops_below_tolerance to expect no tolerance delay for
DEFAULT; locate the test code using the variable _skip_post_adjustments and the
test functions named above and make the change so the test behavior matches the
production logic in calculate_calibration_local().
- Around line 23-340: Replace the duplicated inline calibration logic in these
tests with integration-style calls to the real production functions: call
calculate_calibration_local() and calculate_calibration_setpoint() instead of
reimplementing branches; construct a minimal/mock "self" object (or fixture)
with the required attributes (cur_temp, bt_target_temp, real_trvs, tolerance,
attr_hvac_action, base_setpoint, etc.) to pass into those functions, invoke them
for each calibration_mode (CalibrationMode.DEFAULT, AGGRESIVE_CALIBRATION,
MPC_CALIBRATION, etc.), and assert on the returned calibration values (or
mutated state) matching the expected outcomes; update tests like
test_default_mode_has_tolerance_delay_when_idle,
test_aggressive_mode_skips_tolerance_delay_when_idle,
test_setpoint_default_has_tolerance_delay, etc., to call
calculate_calibration_local() / calculate_calibration_setpoint() and assert
results rather than duplicating the arithmetic.
🧹 Nitpick comments (1)
tests/test_aggressive_calibration.py (1)
305-340: Hysteresis scenario test is a good illustration but usesabs()comparisons unnecessarily.Since all values are computed from exact floating-point arithmetic (no division or irrational operations), exact equality checks would work fine here (as used in all other tests). Using
abs(... - ...) < 0.001is fine but inconsistent with the rest of the file. Minor nit — not blocking.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
custom_components/better_thermostat/utils/controlling.py (1)
416-477:⚠️ Potential issue | 🟠 MajorStale cooler control path in the TRV-unavailable branch lacks new guards and duplicates work.
control_cooleris already called at line 94 (before anycontrol_trvcall). Yet when a TRV is unavailable, this block unconditionally issuesset_temperatureandset_hvac_modeto the cooler again, without any of the new safeguards:
TypeErrorrisk –self.cur_temp,self.bt_target_cooltemp, andself.toleranceare used at lines 418/440 withoutNonechecks (the newcontrol_coolerguards against this at line 167).- Redundant / conflicting commands – The cooler has already been set to
desired_modebycontrol_cooler; this block may flip it back to a different mode because it uses_new_hvac_mode(which factors in window-open state but not the same decision logic).- No state-diffing – Commands are always sent even if the cooler is already in the desired state.
Consider removing this block entirely (consistent with the available-TRV path at line 714, which has already been cleaned up) and relying on the centralized
control_cooler.Proposed fix – remove the duplicate cooler section
_new_hvac_mode = handle_window_open(self, _remapped_states) - # New cooler section - if self.cooler_entity_id is not None: - if ( - self.cur_temp >= self.bt_target_cooltemp - self.tolerance - and _new_hvac_mode is not HVACMode.OFF - and self.cur_temp > self.bt_target_temp - ): - await self.hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": self.cooler_entity_id, - "temperature": self.bt_target_cooltemp, - }, - blocking=True, - context=self.context, - ) - await self.hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": self.cooler_entity_id, "hvac_mode": HVACMode.COOL}, - blocking=True, - context=self.context, - ) - elif ( - self.cur_temp <= (self.bt_target_cooltemp - self.tolerance) - and _new_hvac_mode is not HVACMode.OFF - ): - await self.hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": self.cooler_entity_id, - "temperature": self.bt_target_cooltemp, - }, - blocking=True, - context=self.context, - ) - await self.hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": self.cooler_entity_id, "hvac_mode": HVACMode.OFF}, - blocking=True, - context=self.context, - ) - else: - await self.hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": self.cooler_entity_id, - "temperature": self.bt_target_cooltemp, - }, - blocking=True, - context=self.context, - ) - await self.hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": self.cooler_entity_id, "hvac_mode": HVACMode.OFF}, - blocking=True, - context=self.context, - ) - # if we don't need to heat, we force HVACMode to be off
🤖 Fix all issues with AI agents
In `@custom_components/better_thermostat/events/cooler.py`:
- Around line 38-52: The code accesses new_state.attributes before verifying
types; move the isinstance checks so that both new_state and old_state are
confirmed to be instances of State (using the existing isinstance(new_state,
State) and isinstance(old_state, State) guard) before any access to
new_state.attributes, and only then check if new_state.attributes is None and
log/return as currently done (preserve the existing debug messages referencing
self.device_name and entity_id).
In `@custom_components/better_thermostat/sensor.py`:
- Around line 404-411: The current cleanup logic (looping tracked_pid_numbers
and splitting on "_pid_") is fragile because it reverse-parses unique_id
strings; change the tracking structure (_ACTIVE_PID_NUMBERS and related PID
tracking maps) from a list of unique_id strings to a map/dict keyed by unique_id
with stored metadata { "trv": trv_entity_id, "param": parameter } and update all
places that create PID entities to populate that metadata; then modify cleanup
routines (the loop using tracked_pid_numbers, _cleanup_pid_switch_entities, and
_cleanup_preset_number_entities) to read the stored "trv" and "param" values
directly (e.g., replace the split/replace logic that uses bt_climate.unique_id
and parts = pid_unique_id.split("_pid_")) so you no longer reconstruct
trv_entity_id from the unique_id string.
In `@docs/Configuration/configuration.md`:
- Line 11: Replace the typo "Goto" with "Go to" in the markdown heading text
that currently reads "**Goto: `Settings` -> `Devices & Services` ->
`Integrations` -> `+ Add Integration` -> `Better Thermostat`**" so it becomes
"**Go to: `Settings` -> `Devices & Services` -> `Integrations` -> `+ Add
Integration` -> `Better Thermostat`**"; update the string in configuration.md
accordingly to preserve existing formatting and punctuation.
🧹 Nitpick comments (8)
custom_components/better_thermostat/model_fixes/BTH-RM.py (1)
44-47: Good: parameterized logging replaces f-strings.Note that the sibling file
BTH-RM230Z.py(lines 45 and 71) still uses f-string logging for the equivalent messages. Consider aligning both files for consistency.Also applies to: 72-77
custom_components/better_thermostat/model_fixes/BTH-RM230Z.py (1)
44-45: Inconsistent logging style: f-strings here vs. parameterized logging in BTH-RM.py.
BTH-RM.pywas updated to use%splaceholders (lines 45-47, 73-76), but these two log calls still use f-strings. Parameterized logging defers string interpolation until the message is actually emitted, which is the recommended practice.Proposed fix
_LOGGER.debug( - f"better_thermostat {self.device_name}: TRV {entity_id} device quirk bth-rm230z for set_temperature active" + "better_thermostat %s: TRV %s device quirk bth-rm230z for set_temperature active", + self.device_name, + entity_id, )_LOGGER.debug( - f"better_thermostat {self.device_name}: TRV {entity_id} device quirk bth-rm230z found hvac_modes {hvac_modes}" + "better_thermostat %s: TRV %s device quirk bth-rm230z found hvac_modes %s", + self.device_name, + entity_id, + hvac_modes, )Also applies to: 70-72
custom_components/better_thermostat/utils/controlling.py (1)
320-405: Duplicated valve-control logic across TRV-unavailable and TRV-available paths.The
CalibrationMode-based valve-percent selection (lines 320-405 and 630-706) is nearly identical copy-paste. Any future calibration-mode addition or bug fix must be applied in both places. Consider extracting a helper like_resolve_valve_balance(self, heater_entity_id, calibration_mode, calibration_type).custom_components/better_thermostat/number.py (2)
96-104: Duplicate cleanup of tracking dicts across number.py and sensor.py.Both
number.py(lines 101-102) andsensor.py(lines 566-567) pop the same_ACTIVE_PRESET_NUMBERS[entry_id]and_ACTIVE_PID_NUMBERS[entry_id]. The double-pop is safe but redundant — whichever platform unloads second operates on already-removed keys.This is a consequence of the shared mutable state noted above. If ownership is moved to a shared module, cleanup should be consolidated in one place (e.g.,
__init__.py'sasync_unload_entry).
13-13: Cross-module shared mutable state creates tight coupling.
number.pyimports and mutates_ACTIVE_PRESET_NUMBERSand_ACTIVE_PID_NUMBERSfromsensor.py. While functionally safe (both modules'async_unload_entrymethods safely call.popwith default), the ownership of these tracking structures is unclear and creates cross-module dependency. Consider moving these dicts toutils/const.pyto make the dependency direction explicit and improve code clarity.custom_components/better_thermostat/sensor.py (3)
28-36: German comments mixed with English codebase.Several comments throughout this file are in German (e.g., "Globale Tracking-Variablen für aktive algorithmus-spezifische Entitäten", "Konvertiere String zu Enum falls nötig", "Prüfe auf Änderungen bei den Algorithmen", etc.). For consistency with the rest of the codebase (which uses English), consider translating these to English.
134-158:_ENTITY_CLEANUP_CALLBACKSis stored but never invoked — dead state.Line 151 stores the
_on_config_changecallback in_ENTITY_CLEANUP_CALLBACKS, and line 565 pops it during unload, but nothing in the codebase ever reads or invokes these stored callbacks. The actual cleanup is handled by_DISPATCHER_UNSUBSCRIBES. Consider removing_ENTITY_CLEANUP_CALLBACKSto avoid confusion.Proposed cleanup
-# Store callback für späteren Cleanup -_ENTITY_CLEANUP_CALLBACKS[entry.entry_id] = _on_config_change - # Listen to configuration change signalsAnd in
async_unload_entry:_ACTIVE_ALGORITHM_ENTITIES.pop(entry_id, None) -_ENTITY_CLEANUP_CALLBACKS.pop(entry_id, None) _ACTIVE_PRESET_NUMBERS.pop(entry_id, None)And remove the module-level dict:
_ACTIVE_ALGORITHM_ENTITIES = {} -_ENTITY_CLEANUP_CALLBACKS = {} _DISPATCHER_UNSUBSCRIBES = {}
269-293: CalibrationMode string-to-enum normalization is repeated in 5+ places.The same
isinstance(calibration_mode, str) → CalibrationMode(calibration_mode)try/except pattern appears in_get_active_algorithms(line 280),_cleanup_pid_number_entities(line 392),_cleanup_pid_switch_entities(line 466),number.py(line 68), andconfig_flow.py. Extract a small helper to DRY this up.Suggested helper
In
utils/const.pyalongside theCalibrationModeenum:def normalize_calibration_mode(value) -> CalibrationMode | None: """Normalize a raw calibration mode value to a CalibrationMode enum, or None.""" if isinstance(value, CalibrationMode): return value if isinstance(value, str): try: return CalibrationMode(value) except ValueError: return None return None
- 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 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 **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 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>
* 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
…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
) 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
…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
* 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.
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.
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.
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.
…ler (#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.
…e, fix calibration, add blueprints
…e, fix calibration, add blueprints
|
-> This state is in Beta 13 |
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).
Motivation:
This is the pre release branche
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Tests