From 2d72efe447a0491c642536d5d2717dd29d18badb Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Thu, 7 Nov 2024 21:57:08 +0100 Subject: [PATCH] Issue 600 energy can be negative after configuration (#614) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add logs to diagnose the case * Issue #552 (#608) Co-authored-by: Jean-Marc Collin * Fix typo (#607) * - Force writing state when entity is removed - Fix bug with issue #552 on CONF_USE_CENTRAL_BOILER_FEATURE which should be proposed on a central configuration - Improve reload of entity to avoid reloading all VTherm. Only the reconfigured one will be reloaded --------- Co-authored-by: Jean-Marc Collin Co-authored-by: Ludovic BOUÉ --- .../versatile_thermostat/__init__.py | 11 ++++- .../versatile_thermostat/base_thermostat.py | 49 ++++++++++++++++++- .../versatile_thermostat/commons.py | 1 - .../versatile_thermostat/config_flow.py | 2 +- .../versatile_thermostat/const.py | 6 ++- .../thermostat_climate.py | 17 ++++++- .../versatile_thermostat/thermostat_switch.py | 14 +++++- .../versatile_thermostat/thermostat_valve.py | 14 +++++- .../versatile_thermostat/vtherm_api.py | 6 ++- 9 files changed, 108 insertions(+), 12 deletions(-) diff --git a/custom_components/versatile_thermostat/__init__.py b/custom_components/versatile_thermostat/__init__.py index 5c7c366..c95209d 100644 --- a/custom_components/versatile_thermostat/__init__.py +++ b/custom_components/versatile_thermostat/__init__.py @@ -178,13 +178,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if hass.state == CoreState.running: await api.reload_central_boiler_entities_list() - await api.init_vtherm_links() + await api.init_vtherm_links(entry.entry_id) return True async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener.""" + + _LOGGER.debug( + "Calling update_listener entry: entry_id='%s', value='%s'", + entry.entry_id, + entry.data, + ) + if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG: await reload_all_vtherm(hass) else: @@ -193,7 +200,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) if api is not None: await api.reload_central_boiler_entities_list() - await api.init_vtherm_links() + await api.init_vtherm_links(entry.entry_id) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 7342a12..0b8cc9c 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -19,7 +19,10 @@ from homeassistant.core import ( ) from homeassistant.components.climate import ClimateEntity -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.restore_state import ( + RestoreEntity, + async_get as restore_async_get, +) from homeassistant.helpers.entity import Entity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType @@ -84,6 +87,10 @@ def get_tz(hass: HomeAssistant): return dt_util.get_time_zone(hass.config.time_zone) +_LOGGER_ENERGY = logging.getLogger( + "custom_components.versatile_thermostat.energy_debug" +) + class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): """Representation of a base class for all Versatile Thermostat device.""" @@ -198,6 +205,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): self._attr_translation_key = "versatile_thermostat" self._total_energy = None + _LOGGER_ENERGY.debug("%s - _init_ resetting energy to None", self) # because energy of climate is calculated in the thermostat we have to keep that here and not in underlying entity self._underlying_climate_start_hvac_action_date = None @@ -470,6 +478,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): self._presence_state = None self._total_energy = None + _LOGGER_ENERGY.debug("%s - post_init_ resetting energy to None", self) # Read the parameter from configuration.yaml if it exists short_ema_params = DEFAULT_SHORT_EMA_PARAMS @@ -585,14 +594,24 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): # issue 428. Link to others entities will start at link # await self.async_startup() + async def async_will_remove_from_hass(self): + """Try to force backup of entity""" + _LOGGER_ENERGY.debug( + "%s - force write before remove. Energy is %s", self, self.total_energy + ) + # Force dump in background + await restore_async_get(self.hass).async_dump_states() + def remove_thermostat(self): """Called when the thermostat will be removed""" _LOGGER.info("%s - Removing thermostat", self) + for under in self._underlyings: under.remove_entity() async def async_startup(self, central_configuration): - """Triggered on startup, used to get old state and set internal states accordingly""" + """Triggered on startup, used to get old state and set internal states accordingly. This is triggered by + VTherm API""" _LOGGER.debug("%s - Calling async_startup", self) _LOGGER.debug("%s - Calling async_startup_internal", self) @@ -804,6 +823,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY) self._total_energy = old_total_energy if old_total_energy is not None else 0 + _LOGGER_ENERGY.debug( + "%s - get_my_previous_state restored energy is %s", + self, + self._total_energy, + ) self.restore_specific_previous_state(old_state) else: @@ -817,6 +841,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): "No previously saved temperature, setting to %s", self._target_temp ) self._total_energy = 0 + _LOGGER_ENERGY.debug( + "%s - get_my_previous_state no previous state energy is %s", + self, + self._total_energy, + ) if not self._hvac_mode: self._hvac_mode = HVACMode.OFF @@ -2622,6 +2651,22 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): "hvac_off_reason": self.hvac_off_reason, } + _LOGGER_ENERGY.debug( + "%s - update_custom_attributes saved energy is %s", + self, + self.total_energy, + ) + + @overrides + def async_write_ha_state(self): + """overrides to have log""" + _LOGGER_ENERGY.debug( + "%s - async_write_ha_state written state energy is %s", + self, + self._total_energy, + ) + return super().async_write_ha_state() + @callback def async_registry_entry_updated(self): """update the entity if the config entry have been updated diff --git a/custom_components/versatile_thermostat/commons.py b/custom_components/versatile_thermostat/commons.py index 19a02b3..b76267c 100644 --- a/custom_components/versatile_thermostat/commons.py +++ b/custom_components/versatile_thermostat/commons.py @@ -17,7 +17,6 @@ from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError _LOGGER = logging.getLogger(__name__) - def get_tz(hass: HomeAssistant): """Get the current timezone""" diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index de67f6b..5c0a82a 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -215,7 +215,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): CONF_USE_PRESETS_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG, CONF_USE_CENTRAL_MODE, - CONF_USE_CENTRAL_BOILER_FEATURE, + # CONF_USE_CENTRAL_BOILER_FEATURE, this is for Central Config CONF_USED_BY_CENTRAL_BOILER, ]: if data.get(conf) is True: diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index 44f4838..d6ceb7b 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -354,7 +354,11 @@ CONF_WINDOW_ACTIONS = [ CONF_WINDOW_ECO_TEMP, ] -SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON +SUPPORT_FLAGS = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON +) SERVICE_SET_PRESENCE = "set_presence" SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature" diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 1411f27..f7ef9ed 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -31,6 +31,10 @@ from .auto_start_stop_algorithm import ( ) _LOGGER = logging.getLogger(__name__) +_LOGGER_ENERGY = logging.getLogger( + "custom_components.versatile_thermostat.energy_debug" +) + HVAC_ACTION_ON = [ # pylint: disable=invalid-name HVACAction.COOLING, @@ -97,7 +101,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): """Initialize the Thermostat""" super().post_init(config_entry) - + for climate in config_entry.get(CONF_UNDERLYING_LIST): self._underlyings.append( UnderlyingClimate( @@ -549,6 +553,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): ] = self._auto_start_stop_algo.accumulated_error_threshold self.async_write_ha_state() + _LOGGER.debug( "%s - Calling update_custom_attributes: %s", self, @@ -595,8 +600,18 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): if self._total_energy is None: self._total_energy = added_energy + _LOGGER_ENERGY.debug( + "%s - incremente_energy set energy is %s", + self, + self._total_energy, + ) else: self._total_energy += added_energy + _LOGGER_ENERGY.debug( + "%s - incremente_energy incremented energy is %s", + self, + self._total_energy, + ) _LOGGER.debug( "%s - added energy is %.3f . Total energy is now: %.3f", diff --git a/custom_components/versatile_thermostat/thermostat_switch.py b/custom_components/versatile_thermostat/thermostat_switch.py index 7869d2a..4e07b8d 100644 --- a/custom_components/versatile_thermostat/thermostat_switch.py +++ b/custom_components/versatile_thermostat/thermostat_switch.py @@ -21,7 +21,9 @@ from .underlyings import UnderlyingSwitch from .prop_algorithm import PropAlgorithm _LOGGER = logging.getLogger(__name__) - +_LOGGER_ENERGY = logging.getLogger( + "custom_components.versatile_thermostat.energy_debug" +) class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]): """Representation of a base class for a Versatile Thermostat over a switch.""" @@ -183,8 +185,18 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]): if self._total_energy is None: self._total_energy = added_energy + _LOGGER_ENERGY.debug( + "%s - incremente_energy set energy is %s", + self, + self._total_energy, + ) else: self._total_energy += added_energy + _LOGGER_ENERGY.debug( + "%s - incremente_energy increment energy is %s", + self, + self._total_energy, + ) self.update_custom_attributes() diff --git a/custom_components/versatile_thermostat/thermostat_valve.py b/custom_components/versatile_thermostat/thermostat_valve.py index 7eb7f23..d9324d0 100644 --- a/custom_components/versatile_thermostat/thermostat_valve.py +++ b/custom_components/versatile_thermostat/thermostat_valve.py @@ -25,7 +25,9 @@ from .const import ( from .underlyings import UnderlyingValve _LOGGER = logging.getLogger(__name__) - +_LOGGER_ENERGY = logging.getLogger( + "custom_components.versatile_thermostat.energy_debug" +) class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method """Representation of a class for a Versatile Thermostat over a Valve""" @@ -265,8 +267,18 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a if self._total_energy is None: self._total_energy = added_energy + _LOGGER_ENERGY.debug( + "%s - incremente_energy set energy is %s", + self, + self._total_energy, + ) else: self._total_energy += added_energy + _LOGGER_ENERGY.debug( + "%s - get_my_previous_state increment energy is %s", + self, + self._total_energy, + ) self.update_custom_attributes() diff --git a/custom_components/versatile_thermostat/vtherm_api.py b/custom_components/versatile_thermostat/vtherm_api.py index 52a0609..98cef53 100644 --- a/custom_components/versatile_thermostat/vtherm_api.py +++ b/custom_components/versatile_thermostat/vtherm_api.py @@ -150,10 +150,11 @@ class VersatileThermostatAPI(dict): return entity.state return None - async def init_vtherm_links(self): + async def init_vtherm_links(self, entry_id=None): """Initialize all VTherms entities links This method is called when HA is fully started (and all entities should be initialized) Or when we need to reload all VTherm links (with Number temp entities, central boiler, ...) + If entry_id is set, only the VTherm of this entry will be reloaded """ await self.reload_central_boiler_binary_listener() await self.reload_central_boiler_entities_list() @@ -175,7 +176,8 @@ class VersatileThermostatAPI(dict): entity.device_info and entity.device_info.get("model", None) == DOMAIN ): - await entity.async_startup(self.find_central_configuration()) + if entry_id is None or entry_id == entity.unique_id: + await entity.async_startup(self.find_central_configuration()) async def init_vtherm_preset_with_central(self): """Init all VTherm presets when the VTherm uses central temperature"""