This commit is contained in:
Home Assistant Version Control
2026-08-12 19:11:00 +00:00
parent b654759146
commit 2218539a12
76 changed files with 443 additions and 100 deletions
@@ -85,11 +85,16 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_attr_swing_horizontal_mode = ""
_entity_component_unrecorded_attributes = (
ClimateEntity._entity_component_unrecorded_attributes.union(frozenset({"configuration", "preset_temperatures"}))
ClimateEntity._entity_component_unrecorded_attributes.union(frozenset({"configuration", "preset_temperatures", "specific_states"}))
.union(FeaturePresenceManager.unrecorded_attributes)
.union(FeaturePowerManager.unrecorded_attributes)
.union(FeatureMotionManager.unrecorded_attributes)
.union(FeatureWindowManager.unrecorded_attributes)
.union(FeatureSafetyManager.unrecorded_attributes)
.union(FeatureLockManager.unrecorded_attributes)
.union(FeatureTimedPresetManager.unrecorded_attributes)
.union(FeatureHeatingFailureDetectionManager.unrecorded_attributes)
.union(FeatureRepairIncorrectStateManager.unrecorded_attributes)
)
##
@@ -192,6 +197,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._use_central_config_temperature = False
self._hvac_off_reason: str | None = None
self._hvac_mode_reason: str | None = None
self._hvac_list: list[VThermHvacMode] = []
self._str_hvac_list: list[str] = []
self._temperature_reason: str | None = None
@@ -634,6 +640,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# Try to get total_energy from specific_states (new format) or root level (old format)
specific_states = old_state.attributes.get("specific_states", {})
self._hvac_mode_reason = specific_states.get(HVAC_MODE_REASON_NAME, self._hvac_off_reason)
old_total_energy = specific_states.get(ATTR_TOTAL_ENERGY)
if old_total_energy is None:
# Fallback to root level for backward compatibility
@@ -1280,6 +1287,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
window detection or auto-start-stop"""
return self._hvac_off_reason
@property
def hvac_mode_reason(self) -> str | None:
"""Returns the reason why the current hvac_mode is forced.
Unlike hvac_off_reason, this is set whatever the forced hvac_mode is
(off, fan_only, dry, ...) so the UI can explain why the VTherm is not
in the requested mode."""
return self._hvac_mode_reason
@property
def temperature_reason(self) -> str | None:
"""Returns the reason of the target temperature
@@ -1680,6 +1695,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# issue #1958 - when window_action=fan_only temporarily switches the mode to FAN_ONLY,
# the user's intent (requested_state) is still COOL, so we must use AC presets.
or (self.vtherm_hvac_mode == VThermHvacMode_FAN_ONLY and self.requested_state.hvac_mode == VThermHvacMode_COOL)
# when auto-start/stop temporarily switches the mode to DRY as its stop mode,
# the user's intent (requested_state) is still COOL, so we must use AC presets.
or (self.vtherm_hvac_mode == VThermHvacMode_DRY and self.requested_state.hvac_mode == VThermHvacMode_COOL)
# (self.is_over_switch and self._ac_mode)
# or self.vtherm_hvac_mode == VThermHvacMode_COOL
# or (self.vtherm_hvac_mode == VThermHvacMode_OFF and self.requested_state.hvac_mode == VThermHvacMode_COOL)
@@ -1733,6 +1751,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Set the reason of hvac_off"""
self._hvac_off_reason = hvac_off_reason
def set_hvac_mode_reason(self, hvac_mode_reason: str | None):
"""Set the reason why the current hvac_mode is forced"""
self._hvac_mode_reason = hvac_mode_reason
def set_temperature_reason(self, temperature_reason: str | None):
"""Set the reason of temperature"""
self._temperature_reason = temperature_reason
@@ -1823,6 +1845,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"ema_temp": self._ema_temp,
"temperature_slope": round(self.last_temperature_slope or 0, 3),
"hvac_off_reason": self.hvac_off_reason,
"hvac_mode_reason": self.hvac_mode_reason,
ATTR_TOTAL_ENERGY: self.total_energy,
"last_change_time_from_vtherm": (
self._last_change_time_from_vtherm.astimezone(self._current_tz).isoformat() if self._last_change_time_from_vtherm is not None else None
@@ -2134,7 +2157,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"This thermostat does not use TPI algorithm."
)
async def service_set_auto_tpi_mode(self, auto_tpi_mode: bool):
async def service_set_auto_tpi_mode(
self,
auto_tpi_mode: bool,
reinitialise: bool = True,
allow_kint_boost_on_stagnation: bool = False,
allow_kext_compensation_on_overshoot: bool = False,
):
"""Stub method for Auto TPI mode service on non-TPI thermostats.
This service is only available for switch/valve type thermostats that use TPI algorithm.