Compare commits

..

7 Commits

Author SHA1 Message Date
Jean-Marc Collin
ba69319198 Issue #619 - manual hvac_off should be prioritized over window and auto-start/stop hvac_off (#622)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-10 10:17:53 +01:00
Jean-Marc Collin
f9df925181 Issue #615 - VTherm switch to manual on its own (#618)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-09 18:44:13 +01:00
Jean-Marc Collin
2d72efe447 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>
2024-11-07 21:57:08 +01:00
Ludovic BOUÉ
95af6eba97 Fix typo (#607) 2024-11-05 22:47:42 +01:00
Jean-Marc Collin
06dc537767 Issue #552 (#608)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-11-05 22:39:26 +01:00
Joeri Colman
2d79d961dc Update en.json (#604)
fixed typo
2024-11-05 10:40:49 +01:00
Jean-Marc Collin
027bf8386b Add message into issue template. 2024-11-05 06:37:27 +00:00
16 changed files with 1160 additions and 331 deletions

View File

@@ -152,6 +152,7 @@ climate:
name: Underlying thermostat2 name: Underlying thermostat2
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
ac_mode: false
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat3 name: Underlying thermostat3
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3

View File

@@ -4,6 +4,8 @@ about: Create a report to help us improve
--- ---
> Please read carefuly this instructions and fill this form before writing an issue. It helps me to help you.
<!-- This template will allow the maintainer to be efficient and post the more accurante response as possible. There is many types / modes / configuration possible, so the analysis can be very tricky. If don't follow this template, your issue could be rejected without any message. Please help me to help you. --> <!-- This template will allow the maintainer to be efficient and post the more accurante response as possible. There is many types / modes / configuration possible, so the analysis can be very tricky. If don't follow this template, your issue could be rejected without any message. Please help me to help you. -->
<!-- Before you open a new issue, search through the existing issues to see if others have had the same problem. <!-- Before you open a new issue, search through the existing issues to see if others have had the same problem.

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 ## 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). 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: 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:

View File

@@ -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
@@ -1177,6 +1206,24 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if hvac_mode is None: if hvac_mode is None:
return return
def save_state():
self.reset_last_change_time()
self.update_custom_attributes()
self.async_write_ha_state()
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
# If we already are in OFF, the manual OFF should just overwrite the reason and saved_hvac_mode
if self._hvac_mode == HVACMode.OFF and hvac_mode == HVACMode.OFF:
_LOGGER.info(
"%s - already in OFF. Change the reason to MANUAL and erase the saved_havc_mode"
)
self._hvac_off_reason = HVAC_OFF_REASON_MANUAL
self._saved_hvac_mode = HVACMode.OFF
save_state()
return
self._hvac_mode = hvac_mode self._hvac_mode = hvac_mode
# Delegate to all underlying # Delegate to all underlying
@@ -1198,14 +1245,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# Ensure we update the current operation after changing the mode # Ensure we update the current operation after changing the mode
self.reset_last_temperature_time() self.reset_last_temperature_time()
self.reset_last_change_time()
if self._hvac_mode != HVACMode.OFF: if self._hvac_mode != HVACMode.OFF:
self.set_hvac_off_reason(None) self.set_hvac_off_reason(None)
self.update_custom_attributes() save_state()
self.async_write_ha_state()
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
@overrides @overrides
async def async_set_preset_mode( async def async_set_preset_mode(
@@ -2198,8 +2242,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
save_all() save_all()
if new_central_mode == CENTRAL_MODE_STOPPED: if new_central_mode == CENTRAL_MODE_STOPPED:
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL) if self.hvac_mode != HVACMode.OFF:
await self.async_set_hvac_mode(HVACMode.OFF) self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
await self.async_set_hvac_mode(HVACMode.OFF)
return return
if new_central_mode == CENTRAL_MODE_COOL_ONLY: if new_central_mode == CENTRAL_MODE_COOL_ONLY:
@@ -2213,7 +2258,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if new_central_mode == CENTRAL_MODE_HEAT_ONLY: if new_central_mode == CENTRAL_MODE_HEAT_ONLY:
if HVACMode.HEAT in self.hvac_modes: if HVACMode.HEAT in self.hvac_modes:
await self.async_set_hvac_mode(HVACMode.HEAT) await self.async_set_hvac_mode(HVACMode.HEAT)
else: # if not already off
elif self.hvac_mode != HVACMode.OFF:
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL) self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
await self.async_set_hvac_mode(HVACMode.OFF) await self.async_set_hvac_mode(HVACMode.OFF)
return return
@@ -2622,6 +2668,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

View File

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

View File

@@ -109,17 +109,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
) )
self._infos[CONF_USE_MOTION_FEATURE] = self._infos.get( 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) ) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
self._infos[CONF_USE_POWER_FEATURE] = self._infos.get( self._infos[CONF_USE_POWER_FEATURE] = self._infos.get(
CONF_USE_POWER_CENTRAL_CONFIG CONF_USE_POWER_CENTRAL_CONFIG, False
) or ( ) or (
self._infos.get(CONF_POWER_SENSOR) is not None self._infos.get(CONF_POWER_SENSOR) is not None
and self._infos.get(CONF_MAX_POWER_SENSOR) is not None and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
) )
self._infos[CONF_USE_PRESENCE_FEATURE] = ( 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 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[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 and self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
) )
@@ -145,12 +145,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_PRESETS_CENTRAL_CONFIG, CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG, CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG,
CONF_USE_CENTRAL_MODE,
): ):
if not is_empty: if not is_empty:
current_config = self._infos.get(config, None) 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: else:
self._infos[config] = self._central_config is not None self._infos[config] = self._central_config is not None
@@ -209,6 +214,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_USE_PRESENCE_CENTRAL_CONFIG, CONF_USE_PRESENCE_CENTRAL_CONFIG,
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_BOILER_FEATURE, this is for Central Config
CONF_USED_BY_CENTRAL_BOILER,
]: ]:
if data.get(conf) is True: if data.get(conf) is True:
_LOGGER.error( _LOGGER.error(
@@ -306,6 +314,22 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
): ):
return False 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 return True
def merge_user_input(self, data_schema: vol.Schema, user_input: dict): 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, 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"

View File

@@ -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",
@@ -837,7 +852,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
changes = True changes = True
# try to manage new target temperature set if state if no other changes have been found # try to manage new target temperature set if state if no other changes have been found
if not changes: # and if a target temperature have already been sent
if not changes and under.last_sent_temperature is not None:
_LOGGER.debug( _LOGGER.debug(
"Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s", "Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s",
under.last_sent_temperature, under.last_sent_temperature,

View File

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

View File

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

View File

@@ -64,7 +64,7 @@
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection", "use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page", "use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after selecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
"use_auto_start_stop_feature": "Use the auto start and stop feature" "use_auto_start_stop_feature": "Use the auto start and stop feature"
} }
}, },
@@ -547,4 +547,4 @@
} }
} }
} }
} }

View File

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

View File

@@ -630,6 +630,7 @@ async def test_climate_ac_only_change_central_mode_true(
}, },
) )
# 1. set hvac_mode to COOL and preet ECO
with patch("homeassistant.core.ServiceRegistry.async_call"), patch( with patch("homeassistant.core.ServiceRegistry.async_call"), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,8 @@ from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
) )
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from custom_components.versatile_thermostat.thermostat_climate import ( from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate, ThermostatOverClimate,
) )
@@ -320,6 +322,78 @@ async def test_bug_101(
assert entity.preset_mode is PRESET_NONE assert entity.preset_mode is PRESET_NONE
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_615(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate target temp is changed, the VTherm don't change its own temperature target if no
target_temperature have already been sent"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay
)
# Underlying is in HEAT mode but should be shutdown at startup
fake_underlying_climate = MockClimate(
hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING
)
# 1. create the thermostat
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
assert vtherm
assert vtherm.name == "TheOverClimateMockName"
assert vtherm.is_over_climate is True
assert vtherm.hvac_mode is HVACMode.OFF
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
assert vtherm.hvac_action is HVACAction.HEATING
# Force a preset_mode without sending a temperature (as it was restored with a preset)
vtherm._attr_preset_mode = PRESET_BOOST
assert vtherm.target_temperature == vtherm.min_temp
assert vtherm.preset_mode is PRESET_BOOST
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
# 2. Change the target temp of underlying thermostat at now + 1 min
now = now + timedelta(minutes=1)
await send_climate_change_event_with_temperature(
vtherm,
HVACMode.OFF,
HVACMode.OFF,
HVACAction.OFF,
HVACAction.OFF,
now,
25,
True,
"climate.mock_climate", # the underlying climate entity id
)
# Should NOT have been taken the new target temp nor have change the preset
assert vtherm.target_temperature == vtherm.min_temp
assert vtherm.preset_mode is PRESET_BOOST
mock_underlying_set_hvac_mode.assert_not_called()
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_508( async def test_bug_508(
hass: HomeAssistant, hass: HomeAssistant,
@@ -631,3 +705,356 @@ async def test_ignore_temp_outside_minmax_range(
"climate.mock_climate", # the underlying climate entity id "climate.mock_climate", # the underlying climate entity id
) )
assert entity.target_temperature == 17 assert entity.target_temperature == 17
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_manual_hvac_off_should_take_the_lead_over_window(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test than a manual hvac_off is taken into account over a window hvac_off"""
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
"eco_ac": 27.0,
"comfort_ac": 25.0,
"boost_ac": 23.0,
"frost_away": 7.1,
"eco_away": 17.1,
"comfort_away": 19.1,
"boost_away": 21.1,
"eco_ac_away": 27.1,
"comfort_ac_away": 25.1,
"boost_ac_away": 23.1,
}
config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="overClimateUniqueId",
data={
CONF_NAME: "overClimate",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: True,
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 10,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
},
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mock_climate",
name="mock_climate",
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT],
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm: ThermostatOverClimate = await create_thermostat(
hass, config_entry, "climate.overclimate"
)
assert vtherm is not None
# Initialize all temps
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
# Check correct initialization of auto_start_stop attributes
assert (
vtherm._attr_extra_state_attributes["auto_start_stop_level"]
== AUTO_START_STOP_LEVEL_FAST
)
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
assert enable_entity is not None
assert enable_entity.state == STATE_ON
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# 1. Set mode to Heat and preset to Comfort and close the window
send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
# VTherm should be heating
assert vtherm.hvac_mode == HVACMode.HEAT
# VTherm window_state should be off
assert vtherm.window_state == STATE_OFF
# 2. Open the window and wait for the delay
now = now + timedelta(minutes=2)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
):
vtherm._set_now(now)
try_function = await send_window_change_event(
vtherm, True, False, now, sleep=False
)
await try_function(None)
# Nothing should have change (window event is ignoed as we are already OFF)
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
assert vtherm._saved_hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 2
assert vtherm.window_state == STATE_ON
# 3. Turn off manually the VTherm. This should be taken into account
now = now + timedelta(minutes=1)
vtherm._set_now(now)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
await vtherm.async_set_hvac_mode(HVACMode.OFF)
await hass.async_block_till_done()
# Should be off with reason MANUAL
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_MANUAL
assert vtherm._saved_hvac_mode == HVACMode.OFF
# Window state should not change
assert vtherm.window_state == STATE_ON
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
]
)
# 4. close the window -> we should stay off reason manual
now = now + timedelta(minutes=1)
vtherm._set_now(now)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
):
try_function = await send_window_change_event(
vtherm, False, True, now, sleep=False
)
await try_function(None)
# The VTherm should turn on and off again due to auto-start-stop
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason is HVAC_OFF_REASON_MANUAL
assert vtherm._saved_hvac_mode == HVACMode.OFF
assert vtherm.window_state == STATE_OFF
assert mock_send_event.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test than a manual hvac_off is taken into account over a auto-start/stop hvac_off"""
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
"eco_ac": 27.0,
"comfort_ac": 25.0,
"boost_ac": 23.0,
"frost_away": 7.1,
"eco_away": 17.1,
"comfort_away": 19.1,
"boost_away": 21.1,
"eco_ac_away": 27.1,
"comfort_ac_away": 25.1,
"boost_ac_away": 23.1,
}
config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="overClimateUniqueId",
data={
CONF_NAME: "overClimate",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: True,
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 10,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
},
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mock_climate",
name="mock_climate",
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT],
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm: ThermostatOverClimate = await create_thermostat(
hass, config_entry, "climate.overclimate"
)
assert vtherm is not None
# Initialize all temps
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
# Check correct initialization of auto_start_stop attributes
assert (
vtherm._attr_extra_state_attributes["auto_start_stop_level"]
== AUTO_START_STOP_LEVEL_FAST
)
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
assert enable_entity is not None
assert enable_entity.state == STATE_ON
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# 1. Set mode to Heat and preset to Comfort
send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
# VTherm should be heating
assert vtherm.hvac_mode == HVACMode.HEAT
# 2. Set current temperature to 21 5 min later -> should turn off VTherm
now = now + timedelta(minutes=5)
vtherm._set_now(now)
# reset accumulated error (only for testing)
vtherm._auto_start_stop_algo._accumulated_error = 0
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
await send_temperature_change_event(vtherm, 21, now, True)
await hass.async_block_till_done()
# VTherm should no more be heating
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
assert vtherm._saved_hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 2 # turned to off
mock_send_event.assert_has_calls(
[
call(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
call(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "stop",
"name": "overClimate",
"cause": "Auto stop conditions reached",
"hvac_mode": HVACMode.OFF,
"saved_hvac_mode": HVACMode.HEAT,
"target_temperature": 19.0,
"current_temperature": 21.0,
"temperature_slope": 0.3,
"accumulated_error": -2,
"accumulated_error_threshold": 2,
},
),
]
)
# 3. Turn off manually the VTherm. This should be taken into account
now = now + timedelta(minutes=1)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
await vtherm.async_set_hvac_mode(HVACMode.OFF)
await hass.async_block_till_done()
# Should be off with reason MANUAL
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_MANUAL
assert vtherm._saved_hvac_mode == HVACMode.OFF
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
]
)
# 4. removes the auto-start/stop detection
now = now + timedelta(minutes=5)
vtherm._set_now(now)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
):
await send_temperature_change_event(vtherm, 15, now, True)
await hass.async_block_till_done()
# VTherm should no more be heating
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_MANUAL
assert vtherm._saved_hvac_mode == HVACMode.OFF
assert mock_send_event.call_count == 0 # nothing have change