Issue 600 energy can be negative after configuration (#614)
* Add logs to diagnose the case * Issue #552 (#608) Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com> * 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 <jean-marc.collin-extern@renault.com> Co-authored-by: Ludovic BOUÉ <lboue@users.noreply.github.com>
This commit is contained in:
@@ -178,13 +178,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
if hass.state == CoreState.running:
|
if hass.state == CoreState.running:
|
||||||
await api.reload_central_boiler_entities_list()
|
await api.reload_central_boiler_entities_list()
|
||||||
await api.init_vtherm_links()
|
await api.init_vtherm_links(entry.entry_id)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
"""Update listener."""
|
"""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:
|
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||||
await reload_all_vtherm(hass)
|
await reload_all_vtherm(hass)
|
||||||
else:
|
else:
|
||||||
@@ -193,7 +200,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|||||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||||
if api is not None:
|
if api is not None:
|
||||||
await api.reload_central_boiler_entities_list()
|
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:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ from homeassistant.core import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.components.climate import ClimateEntity
|
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.helpers.entity import Entity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
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)
|
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]):
|
class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||||
"""Representation of a base class for all Versatile Thermostat device."""
|
"""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._attr_translation_key = "versatile_thermostat"
|
||||||
|
|
||||||
self._total_energy = None
|
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
|
# 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
|
self._underlying_climate_start_hvac_action_date = None
|
||||||
@@ -470,6 +478,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
self._presence_state = None
|
self._presence_state = None
|
||||||
|
|
||||||
self._total_energy = 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
|
# Read the parameter from configuration.yaml if it exists
|
||||||
short_ema_params = DEFAULT_SHORT_EMA_PARAMS
|
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
|
# issue 428. Link to others entities will start at link
|
||||||
# await self.async_startup()
|
# 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):
|
def remove_thermostat(self):
|
||||||
"""Called when the thermostat will be removed"""
|
"""Called when the thermostat will be removed"""
|
||||||
_LOGGER.info("%s - Removing thermostat", self)
|
_LOGGER.info("%s - Removing thermostat", self)
|
||||||
|
|
||||||
for under in self._underlyings:
|
for under in self._underlyings:
|
||||||
under.remove_entity()
|
under.remove_entity()
|
||||||
|
|
||||||
async def async_startup(self, central_configuration):
|
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", self)
|
||||||
|
|
||||||
_LOGGER.debug("%s - Calling async_startup_internal", 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)
|
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
|
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)
|
self.restore_specific_previous_state(old_state)
|
||||||
else:
|
else:
|
||||||
@@ -817,6 +841,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
"No previously saved temperature, setting to %s", self._target_temp
|
"No previously saved temperature, setting to %s", self._target_temp
|
||||||
)
|
)
|
||||||
self._total_energy = 0
|
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:
|
if not self._hvac_mode:
|
||||||
self._hvac_mode = HVACMode.OFF
|
self._hvac_mode = HVACMode.OFF
|
||||||
@@ -2622,6 +2651,22 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
"hvac_off_reason": self.hvac_off_reason,
|
"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
|
@callback
|
||||||
def async_registry_entry_updated(self):
|
def async_registry_entry_updated(self):
|
||||||
"""update the entity if the config entry have been updated
|
"""update the entity if the config entry have been updated
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_tz(hass: HomeAssistant):
|
def get_tz(hass: HomeAssistant):
|
||||||
"""Get the current timezone"""
|
"""Get the current timezone"""
|
||||||
|
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
||||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||||
CONF_USE_CENTRAL_MODE,
|
CONF_USE_CENTRAL_MODE,
|
||||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
# CONF_USE_CENTRAL_BOILER_FEATURE, this is for Central Config
|
||||||
CONF_USED_BY_CENTRAL_BOILER,
|
CONF_USED_BY_CENTRAL_BOILER,
|
||||||
]:
|
]:
|
||||||
if data.get(conf) is True:
|
if data.get(conf) is True:
|
||||||
|
|||||||
@@ -354,7 +354,11 @@ CONF_WINDOW_ACTIONS = [
|
|||||||
CONF_WINDOW_ECO_TEMP,
|
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_PRESENCE = "set_presence"
|
||||||
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
|
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ from .auto_start_stop_algorithm import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
_LOGGER_ENERGY = logging.getLogger(
|
||||||
|
"custom_components.versatile_thermostat.energy_debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||||
HVACAction.COOLING,
|
HVACAction.COOLING,
|
||||||
@@ -97,7 +101,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
"""Initialize the Thermostat"""
|
"""Initialize the Thermostat"""
|
||||||
|
|
||||||
super().post_init(config_entry)
|
super().post_init(config_entry)
|
||||||
|
|
||||||
for climate in config_entry.get(CONF_UNDERLYING_LIST):
|
for climate in config_entry.get(CONF_UNDERLYING_LIST):
|
||||||
self._underlyings.append(
|
self._underlyings.append(
|
||||||
UnderlyingClimate(
|
UnderlyingClimate(
|
||||||
@@ -549,6 +553,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
] = self._auto_start_stop_algo.accumulated_error_threshold
|
] = self._auto_start_stop_algo.accumulated_error_threshold
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s - Calling update_custom_attributes: %s",
|
"%s - Calling update_custom_attributes: %s",
|
||||||
self,
|
self,
|
||||||
@@ -595,8 +600,18 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
|
|
||||||
if self._total_energy is None:
|
if self._total_energy is None:
|
||||||
self._total_energy = added_energy
|
self._total_energy = added_energy
|
||||||
|
_LOGGER_ENERGY.debug(
|
||||||
|
"%s - incremente_energy set energy is %s",
|
||||||
|
self,
|
||||||
|
self._total_energy,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._total_energy += added_energy
|
self._total_energy += added_energy
|
||||||
|
_LOGGER_ENERGY.debug(
|
||||||
|
"%s - incremente_energy incremented energy is %s",
|
||||||
|
self,
|
||||||
|
self._total_energy,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s - added energy is %.3f . Total energy is now: %.3f",
|
"%s - added energy is %.3f . Total energy is now: %.3f",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ from .underlyings import UnderlyingSwitch
|
|||||||
from .prop_algorithm import PropAlgorithm
|
from .prop_algorithm import PropAlgorithm
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
_LOGGER_ENERGY = logging.getLogger(
|
||||||
|
"custom_components.versatile_thermostat.energy_debug"
|
||||||
|
)
|
||||||
|
|
||||||
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||||
"""Representation of a base class for a Versatile Thermostat over a switch."""
|
"""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:
|
if self._total_energy is None:
|
||||||
self._total_energy = added_energy
|
self._total_energy = added_energy
|
||||||
|
_LOGGER_ENERGY.debug(
|
||||||
|
"%s - incremente_energy set energy is %s",
|
||||||
|
self,
|
||||||
|
self._total_energy,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._total_energy += added_energy
|
self._total_energy += added_energy
|
||||||
|
_LOGGER_ENERGY.debug(
|
||||||
|
"%s - incremente_energy increment energy is %s",
|
||||||
|
self,
|
||||||
|
self._total_energy,
|
||||||
|
)
|
||||||
|
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ from .const import (
|
|||||||
from .underlyings import UnderlyingValve
|
from .underlyings import UnderlyingValve
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
_LOGGER_ENERGY = logging.getLogger(
|
||||||
|
"custom_components.versatile_thermostat.energy_debug"
|
||||||
|
)
|
||||||
|
|
||||||
class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method
|
class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method
|
||||||
"""Representation of a class for a Versatile Thermostat over a Valve"""
|
"""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:
|
if self._total_energy is None:
|
||||||
self._total_energy = added_energy
|
self._total_energy = added_energy
|
||||||
|
_LOGGER_ENERGY.debug(
|
||||||
|
"%s - incremente_energy set energy is %s",
|
||||||
|
self,
|
||||||
|
self._total_energy,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._total_energy += added_energy
|
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()
|
self.update_custom_attributes()
|
||||||
|
|
||||||
|
|||||||
@@ -150,10 +150,11 @@ class VersatileThermostatAPI(dict):
|
|||||||
return entity.state
|
return entity.state
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def init_vtherm_links(self):
|
async def init_vtherm_links(self, entry_id=None):
|
||||||
"""Initialize all VTherms entities links
|
"""Initialize all VTherms entities links
|
||||||
This method is called when HA is fully started (and all entities should be initialized)
|
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, ...)
|
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_binary_listener()
|
||||||
await self.reload_central_boiler_entities_list()
|
await self.reload_central_boiler_entities_list()
|
||||||
@@ -175,7 +176,8 @@ class VersatileThermostatAPI(dict):
|
|||||||
entity.device_info
|
entity.device_info
|
||||||
and entity.device_info.get("model", None) == DOMAIN
|
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):
|
async def init_vtherm_preset_with_central(self):
|
||||||
"""Init all VTherm presets when the VTherm uses central temperature"""
|
"""Init all VTherm presets when the VTherm uses central temperature"""
|
||||||
|
|||||||
Reference in New Issue
Block a user