From 2fc682f1024d2966e468952a1db5967748e0435e Mon Sep 17 00:00:00 2001 From: sayam93 <163408168+sayam93@users.noreply.github.com> Date: Wed, 25 Dec 2024 02:30:57 +0530 Subject: [PATCH] fix: Optimize state updates and ensure ON_OFF feature support for HVACMode.OFF Code optimizations to prevent redundant state updates by updating entity attributes only when actual changes occur, reducing unnecessary state writes. It separates internal updates of entity attributes from those directly changed by users or automations. Previously, it was possible that the climate entities would not restore state properly on Home Assistant restarts This further resolves a warning related in Home Assistant related to entities that implement HVACMode.OFF but do not explicitly declare the ClimateEntityFeature.ON_OFF feature. To ensure compatibility across Home Assistant versions, it dynamically adds the ON_OFF feature only if HVACMode.OFF exists for the templated entity. Fixes https://github.com/jcwillox/hass-template-climate/issues/61 https://github.com/jcwillox/hass-template-climate/issues/76 and https://github.com/jcwillox/hass-template-climate/issues/79 Code formatted by black. --- custom_components/climate_template/climate.py | 138 ++++++++++++------ 1 file changed, 93 insertions(+), 45 deletions(-) diff --git a/custom_components/climate_template/climate.py b/custom_components/climate_template/climate.py index 1e56c65..008d828 100644 --- a/custom_components/climate_template/climate.py +++ b/custom_components/climate_template/climate.py @@ -232,6 +232,31 @@ def __init__(self, hass: HomeAssistant, config: ConfigType): self._unit_of_measurement = hass.config.units.temperature_unit self._attr_supported_features = 0 + if HVACMode.OFF in config[CONF_MODE_LIST] and len(config[CONF_MODE_LIST]) > 1: + if not hasattr(ClimateEntityFeature, "ON_OFF"): + ON_OFF_FEATURE = 1 << 8 + else: + ON_OFF_FEATURE = ClimateEntityFeature.ON_OFF + self._attr_supported_features |= ON_OFF_FEATURE + + # Dynamically add turn_on and turn_off methods if needed + async def async_turn_on(self): + """Turn the climate device on.""" + if HVACMode.OFF in self._attr_hvac_modes: + self._current_operation = next( + mode for mode in self._attr_hvac_modes if mode != HVACMode.OFF + ) + self.async_write_ha_state() + + async def async_turn_off(self): + """Turn the climate device off.""" + if HVACMode.OFF in self._attr_hvac_modes: + self._current_operation = HVACMode.OFF + self.async_write_ha_state() + + self.async_turn_on = async_turn_on.__get__(self) + self.async_turn_off = async_turn_off.__get__(self) + self._attr_hvac_modes = config[CONF_MODE_LIST] self._attr_fan_modes = config[CONF_FAN_MODE_LIST] self._attr_preset_modes = config[CONF_PRESET_MODE_LIST] @@ -539,51 +564,55 @@ def _update_max_humidity(self, humidity): def _update_target_humidity(self, humidity): if humidity not in (STATE_UNKNOWN, STATE_UNAVAILABLE): try: - self._target_humidity = float(humidity) - self.hass.async_create_task( - self.async_set_humidity(self._target_humidity) - ) + new_humidity = float(humidity) + if ( + new_humidity != self._target_humidity + ): # Only update if there's a change + self._target_humidity = new_humidity + self.async_write_ha_state() # Update HA state without triggering an action except ValueError: _LOGGER.error("Could not parse target humidity from %s", humidity) def _update_target_temp(self, temp): if temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): try: - self._target_temp = float(temp) - self.hass.async_create_task( - self.async_set_temperature(**{ATTR_TEMPERATURE: self._target_temp}) - ) + # Update the internal state without triggering the set_temperature action + new_target_temp = float(temp) + if ( + new_target_temp != self._target_temp + ): # Only update if there's a change + self._target_temp = new_target_temp + self.async_write_ha_state() # Update the HA state without triggering an action except ValueError: _LOGGER.error("Could not parse temperature from %s", temp) def _update_target_temp_high(self, temp): if temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): try: - self._attr_target_temperature_high = float(temp) - self.hass.async_create_task( - self.async_set_temperature( - **{ATTR_TARGET_TEMP_HIGH: self._attr_target_temperature_high} - ) - ) + # Update the internal state without triggering the set_temperature action + new_target_temp_high = float(temp) + if new_target_temp_high != self._attr_target_temperature_high: + self._attr_target_temperature_high = new_target_temp_high + self.async_write_ha_state() except ValueError: _LOGGER.error("Could not parse temperature high from %s", temp) def _update_target_temp_low(self, temp): if temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): try: - self._attr_target_temperature_low = float(temp) - self.hass.async_create_task( - self.async_set_temperature( - **{ATTR_TARGET_TEMP_LOW: self._attr_target_temperature_low} - ) - ) + # Update the internal state without triggering the set_temperature action + new_target_temp_low = float(temp) + if new_target_temp_low != self._attr_target_temperature_low: + self._attr_target_temperature_low = new_target_temp_low + self.async_write_ha_state() except ValueError: _LOGGER.error("Could not parse temperature low from %s", temp) def _update_hvac_mode(self, hvac_mode): if hvac_mode in self._attr_hvac_modes: - self._current_operation = hvac_mode - self.hass.async_create_task(self.async_set_hvac_mode(hvac_mode)) + if self._current_operation != hvac_mode: # Only update if there's a change + self._current_operation = hvac_mode + self.async_write_ha_state() # Update HA state without triggering an action elif hvac_mode not in (STATE_UNKNOWN, STATE_UNAVAILABLE): _LOGGER.error( "Received invalid hvac mode: %s. Expected: %s.", @@ -593,8 +622,11 @@ def _update_hvac_mode(self, hvac_mode): def _update_preset_mode(self, preset_mode): if preset_mode in self._attr_preset_modes: - self._current_preset_mode = preset_mode - self.hass.async_create_task(self.async_set_preset_mode(preset_mode)) + if ( + self._current_preset_mode != preset_mode + ): # Only update if there's a change + self._current_preset_mode = preset_mode + self.async_write_ha_state() # Update HA state without triggering an action elif preset_mode not in (STATE_UNKNOWN, STATE_UNAVAILABLE): _LOGGER.error( "Received invalid preset mode %s. Expected %s.", @@ -604,8 +636,9 @@ def _update_preset_mode(self, preset_mode): def _update_fan_mode(self, fan_mode): if fan_mode in self._attr_fan_modes: - self._current_fan_mode = fan_mode - self.hass.async_create_task(self.async_set_fan_mode(fan_mode)) + if self._current_fan_mode != fan_mode: # Only update if there's a change + self._current_fan_mode = fan_mode + self.async_write_ha_state() # Update HA state without triggering an action elif fan_mode not in (STATE_UNKNOWN, STATE_UNAVAILABLE): _LOGGER.error( "Received invalid fan mode: %s. Expected: %s.", @@ -615,10 +648,11 @@ def _update_fan_mode(self, fan_mode): def _update_swing_mode(self, swing_mode): if swing_mode in self._swing_modes_list: - # check swing mode actually changed - if self._current_swing_mode != swing_mode: + if ( + self._current_swing_mode != swing_mode + ): # Only update if there's a change self._current_swing_mode = swing_mode - self.hass.async_create_task(self.async_set_swing_mode(swing_mode)) + self.async_write_ha_state() # Update HA state without triggering an action elif swing_mode not in (STATE_UNKNOWN, STATE_UNAVAILABLE): _LOGGER.error( "Received invalid swing mode: %s. Expected: %s.", @@ -631,8 +665,9 @@ def _update_hvac_action(self, hvac_action): hvac_action in [member.value for member in HVACAction] or hvac_action is None ): - if self._attr_hvac_action != hvac_action: + if self._attr_hvac_action != hvac_action: # Only update if there's a change self._attr_hvac_action = hvac_action + self.async_write_ha_state() # Update HA state without triggering an action elif hvac_action not in (STATE_UNKNOWN, STATE_UNAVAILABLE): _LOGGER.error( "Received invalid hvac action: %s. Expected: %s.", @@ -772,29 +807,42 @@ async def async_set_swing_mode(self, swing_mode: str) -> None: ) async def async_set_temperature(self, **kwargs) -> None: - """Set new target temperature.""" - # handle optimistic mode - if kwargs.get(ATTR_HVAC_MODE, self._current_operation) == HVACMode.HEAT_COOL: - if self._target_temperature_high_template is None: - self._attr_target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + """Set new target temperature explicitly triggered by user or automation.""" + updated = False - if self._target_temperature_low_template is None: - self._attr_target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + if kwargs.get(ATTR_HVAC_MODE, self._current_operation) == HVACMode.HEAT_COOL: + # Explicitly update high and low target temperatures if provided + high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) + low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) if ( - self._target_temperature_high_template - or self._target_temperature_low_template + high_temp is not None + and high_temp != self._attr_target_temperature_high ): - self.async_write_ha_state() + self._attr_target_temperature_high = high_temp + updated = True + + if low_temp is not None and low_temp != self._attr_target_temperature_low: + self._attr_target_temperature_low = low_temp + updated = True + else: - if self._target_temperature_template is None: - self._target_temp = kwargs.get(ATTR_TEMPERATURE) - self.async_write_ha_state() + # Explicitly update single target temperature if provided + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None and temp != self._target_temp: + self._target_temp = temp + updated = True + + # Update Home Assistant state if any changes occurred + if updated: + self.async_write_ha_state() - # set temperature calls can contain a new hvac mode. + # Handle potential HVAC mode change if operation_mode := kwargs.get(ATTR_HVAC_MODE): - await self.async_set_hvac_mode(operation_mode) + if operation_mode != self._current_operation: + await self.async_set_hvac_mode(operation_mode) + # Run the set temperature script if defined if self._set_temperature_script is not None: await self._set_temperature_script.async_run( run_variables={