Feature 234 add central boiler helper (#352)
* Creation of the central boiler config + binary_sensor entity * Fonctional before testu. Miss the service call * Full featured but without testu * Documentation and release. * Add events in README * FIX #341 - when window state change, open_valve_percent should be resend * Issue #343 - disable safety mode for outdoor thermometer * Issue #255 - Specify window action on window open detection * Add en and string translation * central boiler - add entites to fine tune the boiler start * With testu ok * Add testus for valve and climate * Add testus in pipelines * With pip 3 * With more pytest options * Ass coverage tests * Add coverage report in github * Release 5.3.0 --------- Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
This commit is contained in:
@@ -121,6 +121,7 @@ from .const import (
|
||||
CENTRAL_MODE_HEAT_ONLY,
|
||||
CENTRAL_MODE_COOL_ONLY,
|
||||
CENTRAL_MODE_FROST_PROTECTION,
|
||||
send_vtherm_event,
|
||||
)
|
||||
|
||||
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
@@ -196,6 +197,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"window_auto_open_threshold",
|
||||
"window_auto_close_threshold",
|
||||
"window_auto_max_duration",
|
||||
"window_action",
|
||||
"motion_sensor_entity_id",
|
||||
"presence_sensor_entity_id",
|
||||
"power_sensor_entity_id",
|
||||
@@ -203,6 +205,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"temperature_unit",
|
||||
"is_device_active",
|
||||
"target_temperature_step",
|
||||
"is_used_by_central_boiler",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -266,8 +269,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._window_auto_state = False
|
||||
self._window_auto_on = False
|
||||
self._window_auto_algo = None
|
||||
# PR - Adding Window ByPass
|
||||
self._window_bypass_state = False
|
||||
self._window_action = None
|
||||
|
||||
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
|
||||
|
||||
@@ -283,6 +286,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self._is_central_mode = None
|
||||
self._last_central_mode = None
|
||||
self._is_used_by_central_boiler = False
|
||||
|
||||
self.post_init(entry_infos)
|
||||
|
||||
def clean_central_config_doublon(self, config_entry, central_config) -> dict:
|
||||
@@ -472,7 +477,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._saved_preset_mode = PRESET_NONE
|
||||
|
||||
# Power management
|
||||
self._device_power = entry_infos.get(CONF_DEVICE_POWER)
|
||||
self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0
|
||||
self._pmax_on = False
|
||||
self._current_power = None
|
||||
self._current_power_max = None
|
||||
@@ -569,6 +574,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
entry_infos.get(CONF_USE_CENTRAL_MODE) is False
|
||||
) # Default value (None) is True
|
||||
|
||||
self._is_used_by_central_boiler = (
|
||||
entry_infos.get(CONF_USED_BY_CENTRAL_BOILER) is True
|
||||
)
|
||||
|
||||
self._window_action = (
|
||||
entry_infos.get(CONF_WINDOW_ACTION) or CONF_WINDOW_TURN_OFF
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Creation of a new VersatileThermostat entity: unique_id=%s",
|
||||
self,
|
||||
@@ -1010,6 +1023,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
action = HVACAction.HEATING
|
||||
return action
|
||||
|
||||
@property
|
||||
def is_used_by_central_boiler(self) -> HVACAction | None:
|
||||
"""Return true is the VTherm is configured to be used by
|
||||
central boiler"""
|
||||
return self._is_used_by_central_boiler
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
@@ -1079,6 +1098,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Get the Window Bypass"""
|
||||
return self._window_bypass_state
|
||||
|
||||
@property
|
||||
def window_action(self) -> bool | None:
|
||||
"""Get the Window Action"""
|
||||
return self._window_action
|
||||
|
||||
@property
|
||||
def security_state(self) -> bool | None:
|
||||
"""Get the security_state"""
|
||||
@@ -1142,6 +1166,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Returns the number of underlying entities"""
|
||||
return len(self._underlyings)
|
||||
|
||||
@property
|
||||
def underlying_entities(self) -> int:
|
||||
"""Returns the underlying entities"""
|
||||
return self._underlyings
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""True if the VTherm is on (! HVAC_OFF)"""
|
||||
@@ -1468,29 +1497,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self._window_state = new_state.state == STATE_ON
|
||||
|
||||
# PR - Adding Window ByPass
|
||||
_LOGGER.debug("%s - Window ByPass is : %s", self, self._window_bypass_state)
|
||||
if self._window_bypass_state:
|
||||
_LOGGER.info(
|
||||
"%s - Window ByPass is activated. Ignore window event", self
|
||||
)
|
||||
else:
|
||||
if not self._window_state:
|
||||
_LOGGER.info(
|
||||
"%s - Window is closed. Restoring hvac_mode '%s' if central_mode is not STOPPED",
|
||||
self,
|
||||
self._saved_hvac_mode,
|
||||
)
|
||||
if self.last_central_mode != CENTRAL_MODE_STOPPED:
|
||||
await self.restore_hvac_mode(True)
|
||||
elif self._window_state:
|
||||
_LOGGER.info(
|
||||
"%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF
|
||||
)
|
||||
if self.last_central_mode in [CENTRAL_MODE_AUTO, None]:
|
||||
self.save_hvac_mode()
|
||||
await self.change_window_detection_state(self._window_state)
|
||||
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
self.update_custom_attributes()
|
||||
|
||||
if new_state is None or old_state is None or new_state.state == old_state.state:
|
||||
@@ -1619,6 +1633,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
for under in self._underlyings:
|
||||
await under.check_initial_state(self._hvac_mode)
|
||||
|
||||
# Starts the initial control loop (don't wait for an update of temperature)
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
@callback
|
||||
async def _async_update_temp(self, state: State):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
@@ -1644,7 +1661,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
# try to restart if we were in safety mode
|
||||
if self._security_state:
|
||||
await self.check_security()
|
||||
await self.check_safety()
|
||||
|
||||
# check window_auto
|
||||
return await self._async_manage_window_auto()
|
||||
@@ -1671,7 +1688,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
# try to restart if we were in safety mode
|
||||
if self._security_state:
|
||||
await self.check_security()
|
||||
await self.check_safety()
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update external temperature from sensor: %s", ex)
|
||||
|
||||
@@ -1828,7 +1845,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
# Set attributes
|
||||
self._window_auto_state = False
|
||||
await self.restore_hvac_mode(True)
|
||||
await self.change_window_detection_state(self._window_auto_state)
|
||||
# await self.restore_hvac_mode(True)
|
||||
|
||||
if self._window_call_cancel:
|
||||
self._window_call_cancel()
|
||||
@@ -1855,7 +1873,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
if self.window_bypass_state or not self.is_window_auto_enabled:
|
||||
_LOGGER.info("%s - Window auto event is ignored because bypass is ON or window auto detection is disabled", self)
|
||||
_LOGGER.info(
|
||||
"%s - Window auto event is ignored because bypass is ON or window auto detection is disabled",
|
||||
self,
|
||||
)
|
||||
return
|
||||
|
||||
if (
|
||||
@@ -1885,8 +1906,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
# Set attributes
|
||||
self._window_auto_state = True
|
||||
self.save_hvac_mode()
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
await self.change_window_detection_state(self._window_auto_state)
|
||||
# self.save_hvac_mode()
|
||||
# await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
# Arm the end trigger
|
||||
if self._window_call_cancel:
|
||||
@@ -2106,7 +2128,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Get now. The local datetime or the overloaded _set_now date"""
|
||||
return self._now if self._now is not None else datetime.now(self._current_tz)
|
||||
|
||||
async def check_security(self) -> bool:
|
||||
async def check_safety(self) -> bool:
|
||||
"""Check if last temperature date is too long"""
|
||||
now = self.now
|
||||
delta_temp = (
|
||||
@@ -2118,9 +2140,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
mode_cond = self._hvac_mode != HVACMode.OFF
|
||||
|
||||
temp_cond: bool = (
|
||||
delta_temp > self._security_delay_min
|
||||
or delta_ext_temp > self._security_delay_min
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
|
||||
is_outdoor_checked = (
|
||||
not api.safety_mode
|
||||
or api.safety_mode.get("check_outdoor_sensor") is not False
|
||||
)
|
||||
|
||||
temp_cond: bool = delta_temp > self._security_delay_min or (
|
||||
is_outdoor_checked and delta_ext_temp > self._security_delay_min
|
||||
)
|
||||
climate_cond: bool = self.is_over_climate and self.hvac_action not in [
|
||||
HVACAction.COOLING,
|
||||
@@ -2264,6 +2291,63 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
should have found the underlying climate to be operational"""
|
||||
return True
|
||||
|
||||
async def change_window_detection_state(self, new_state):
|
||||
"""Change the window detection state.
|
||||
new_state is on if an open window have been detected or off else
|
||||
"""
|
||||
if not new_state:
|
||||
_LOGGER.info(
|
||||
"%s - Window is closed. Restoring hvac_mode '%s' if central_mode is not STOPPED",
|
||||
self,
|
||||
self._saved_hvac_mode,
|
||||
)
|
||||
if self._window_action in [CONF_WINDOW_FROST_TEMP, CONF_WINDOW_ECO_TEMP]:
|
||||
await self._async_internal_set_temperature(self._saved_target_temp)
|
||||
# default to TURN_OFF
|
||||
elif self._window_action in [CONF_WINDOW_TURN_OFF, CONF_WINDOW_FAN_ONLY]:
|
||||
if self.last_central_mode != CENTRAL_MODE_STOPPED:
|
||||
await self.restore_hvac_mode(True)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"%s - undefined window_action %s. Please open a bug in the github of this project with this log",
|
||||
self,
|
||||
self._window_action,
|
||||
)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF
|
||||
)
|
||||
if self.last_central_mode in [CENTRAL_MODE_AUTO, None]:
|
||||
if self._window_action in [CONF_WINDOW_TURN_OFF, CONF_WINDOW_FAN_ONLY]:
|
||||
self.save_hvac_mode()
|
||||
elif self._window_action in [
|
||||
CONF_WINDOW_FROST_TEMP,
|
||||
CONF_WINDOW_ECO_TEMP,
|
||||
]:
|
||||
self._saved_target_temp = self._target_temp
|
||||
|
||||
if (
|
||||
self._window_action == CONF_WINDOW_FAN_ONLY
|
||||
and HVACMode.FAN_ONLY in self.hvac_modes
|
||||
):
|
||||
await self.async_set_hvac_mode(HVACMode.FAN_ONLY)
|
||||
elif (
|
||||
self._window_action == CONF_WINDOW_FROST_TEMP
|
||||
and self._presets.get(PRESET_FROST_PROTECTION) is not None
|
||||
):
|
||||
await self._async_internal_set_temperature(
|
||||
self.find_preset_temp(PRESET_FROST_PROTECTION)
|
||||
)
|
||||
elif (
|
||||
self._window_action == CONF_WINDOW_ECO_TEMP
|
||||
and self._presets.get(PRESET_ECO) is not None
|
||||
):
|
||||
await self._async_internal_set_temperature(
|
||||
self.find_preset_temp(PRESET_ECO)
|
||||
)
|
||||
else: # default is to turn_off
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
async def async_control_heating(self, force=False, _=None):
|
||||
"""The main function used to run the calculation at each cycle"""
|
||||
|
||||
@@ -2291,7 +2375,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.debug("%s - End of cycle (overpowering)", self)
|
||||
return True
|
||||
|
||||
security: bool = await self.check_security()
|
||||
security: bool = await self.check_safety()
|
||||
if security and self.is_over_climate:
|
||||
_LOGGER.debug("%s - End of cycle (security and over climate)", self)
|
||||
return True
|
||||
@@ -2357,8 +2441,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self.get_preset_away_name(PRESET_COMFORT)
|
||||
),
|
||||
"power_temp": self._power_temp,
|
||||
# Already in super class - "target_temp": self.target_temperature,
|
||||
# Already in super class - "current_temp": self._cur_temp,
|
||||
"target_temperature_step": self.target_temperature_step,
|
||||
"ext_current_temperature": self._cur_ext_temp,
|
||||
"ac_mode": self._ac_mode,
|
||||
@@ -2367,12 +2449,23 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"saved_preset_mode": self._saved_preset_mode,
|
||||
"saved_target_temp": self._saved_target_temp,
|
||||
"saved_hvac_mode": self._saved_hvac_mode,
|
||||
"window_state": self.window_state,
|
||||
"motion_sensor_entity_id": self._motion_sensor_entity_id,
|
||||
"motion_state": self._motion_state,
|
||||
"power_sensor_entity_id": self._power_sensor_entity_id,
|
||||
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
|
||||
"overpowering_state": self.overpowering_state,
|
||||
"presence_sensor_entity_id": self._presence_sensor_entity_id,
|
||||
"presence_state": self._presence_state,
|
||||
"window_state": self.window_state,
|
||||
"window_auto_state": self.window_auto_state,
|
||||
"window_bypass_state": self._window_bypass_state,
|
||||
"window_sensor_entity_id": self._window_sensor_entity_id,
|
||||
"window_delay_sec": self._window_delay_sec,
|
||||
"window_auto_enabled": self.is_window_auto_enabled,
|
||||
"window_auto_open_threshold": self._window_auto_open_threshold,
|
||||
"window_auto_close_threshold": self._window_auto_close_threshold,
|
||||
"window_auto_max_duration": self._window_auto_max_duration,
|
||||
"window_action": self.window_action,
|
||||
"security_delay_min": self._security_delay_min,
|
||||
"security_min_on_percent": self._security_min_on_percent,
|
||||
"security_default_on_percent": self._security_default_on_percent,
|
||||
@@ -2391,19 +2484,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
.astimezone(self._current_tz)
|
||||
.isoformat(),
|
||||
"timezone": str(self._current_tz),
|
||||
"window_sensor_entity_id": self._window_sensor_entity_id,
|
||||
"window_delay_sec": self._window_delay_sec,
|
||||
"window_auto_enabled": self.is_window_auto_enabled,
|
||||
"window_auto_open_threshold": self._window_auto_open_threshold,
|
||||
"window_auto_close_threshold": self._window_auto_close_threshold,
|
||||
"window_auto_max_duration": self._window_auto_max_duration,
|
||||
"motion_sensor_entity_id": self._motion_sensor_entity_id,
|
||||
"presence_sensor_entity_id": self._presence_sensor_entity_id,
|
||||
"power_sensor_entity_id": self._power_sensor_entity_id,
|
||||
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
|
||||
"temperature_unit": self.temperature_unit,
|
||||
"is_device_active": self.is_device_active,
|
||||
"ema_temp": self._ema_temp,
|
||||
"is_used_by_central_boiler": self.is_used_by_central_boiler,
|
||||
}
|
||||
|
||||
@callback
|
||||
@@ -2526,8 +2610,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
def send_event(self, event_type: EventType, data: dict):
|
||||
"""Send an event"""
|
||||
_LOGGER.info("%s - Sending event %s with data: %s", self, event_type, data)
|
||||
data["entity_id"] = self.entity_id
|
||||
data["name"] = self.name
|
||||
data["state_attributes"] = self.state_attributes
|
||||
self._hass.bus.fire(event_type.value, data)
|
||||
send_vtherm_event(self._hass, event_type=event_type, entity=self, data=data)
|
||||
# _LOGGER.info("%s - Sending event %s with data: %s", self, event_type, data)
|
||||
# data["entity_id"] = self.entity_id
|
||||
# data["name"] = self.name
|
||||
# data["state_attributes"] = self.state_attributes
|
||||
# self._hass.bus.fire(event_type.value, data)
|
||||
|
||||
Reference in New Issue
Block a user