Compare commits

...

4 Commits

Author SHA1 Message Date
Jean-Marc Collin
4a97a2faf2 - 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
2024-11-06 19:05:38 +00:00
Ludovic BOUÉ
75dd2be2b1 Fix typo (#607) 2024-11-06 06:04:35 +00:00
Jean-Marc Collin
c2f53a8232 Issue #552 (#608)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-06 06:04:34 +00:00
Jean-Marc Collin
ae9b065387 Add logs to diagnose the case 2024-11-05 18:40:14 +00:00
11 changed files with 701 additions and 321 deletions

View File

@@ -1353,7 +1353,7 @@ Example of graph obtained with Plotly :
## And always better and better with the NOTIFIER daemon app to notify events
This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https ://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats.
This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats.
This is a great example of using the notifications described here [notification](#notifications).

View File

@@ -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:

View File

@@ -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

View File

@@ -17,7 +17,6 @@ from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
_LOGGER = logging.getLogger(__name__)
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""

View File

@@ -109,17 +109,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
)
self._infos[CONF_USE_MOTION_FEATURE] = self._infos.get(
CONF_USE_MOTION_FEATURE
CONF_USE_MOTION_FEATURE, False
) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
self._infos[CONF_USE_POWER_FEATURE] = self._infos.get(
CONF_USE_POWER_CENTRAL_CONFIG
CONF_USE_POWER_CENTRAL_CONFIG, False
) or (
self._infos.get(CONF_POWER_SENSOR) is not None
and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
)
self._infos[CONF_USE_PRESENCE_FEATURE] = (
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG)
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False)
or self._infos.get(CONF_PRESENCE_SENSOR) is not None
)
@@ -129,7 +129,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
)
self._infos[CONF_USE_AUTO_START_STOP_FEATURE] = (
self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE) is True
self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False) is True
and self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
)
@@ -145,12 +145,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG,
CONF_USE_CENTRAL_MODE,
):
if not is_empty:
current_config = self._infos.get(config, None)
self._infos[config] = current_config is True or (
current_config is None and self._central_config is not None
self._infos[config] = self._central_config is not None and (
current_config is True or current_config is None
)
# self._infos[config] = current_config is True or (
# current_config is None and self._central_config is not None
# )
else:
self._infos[config] = self._central_config is not None
@@ -209,6 +214,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG,
CONF_USE_CENTRAL_MODE,
# CONF_USE_CENTRAL_BOILER_FEATURE, this is for Central Config
CONF_USED_BY_CENTRAL_BOILER,
]:
if data.get(conf) is True:
_LOGGER.error(
@@ -306,6 +314,22 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
):
return False
if (
infos.get(CONF_PROP_FUNCTION, None) == PROPORTIONAL_FUNCTION_TPI
and infos.get(CONF_USE_TPI_CENTRAL_CONFIG, False) is False
and (
infos.get(CONF_TPI_COEF_INT, None) is None
or infos.get(CONF_TPI_COEF_EXT) is None
)
):
return False
if (
infos.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False) is True
and self._central_config is None
):
return False
return True
def merge_user_input(self, data_schema: vol.Schema, user_input: dict):

View File

@@ -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"

View File

@@ -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",

View File

@@ -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()

View File

@@ -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()

View File

@@ -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"""

File diff suppressed because it is too large Load Diff