Compare commits
9 Commits
6.8.0.beta
...
6.8.2.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b26d40cec | ||
|
|
e9dcb21093 | ||
|
|
980c24c939 | ||
|
|
56cdbf23bc | ||
|
|
ac406ff1f4 | ||
|
|
146f5dfcab | ||
|
|
7103af3634 | ||
|
|
46da1d6fbe | ||
|
|
3529607948 |
@@ -91,6 +91,48 @@ input_number:
|
||||
icon: mdi:thermostat
|
||||
unit_of_measurement: °C
|
||||
mode: box
|
||||
fake_offset_calibration1:
|
||||
name: Sonoff offset calibration 1
|
||||
min: -12
|
||||
max: 12
|
||||
icon: mdi:tune
|
||||
unit_of_measurement: °C
|
||||
mode: box
|
||||
fake_opening_degree1:
|
||||
name: Sonoff Opening degree 1
|
||||
min: 0
|
||||
max: 100
|
||||
icon: mdi:valve-open
|
||||
unit_of_measurement: "%"
|
||||
mode: box
|
||||
fake_closing_degree1:
|
||||
name: Sonoff Closing degree 1
|
||||
min: 0
|
||||
max: 100
|
||||
icon: mdi:valve-closed
|
||||
unit_of_measurement: "%"
|
||||
mode: box
|
||||
fake_offset_calibration2:
|
||||
name: Sonoff offset calibration 2
|
||||
min: -12
|
||||
max: 12
|
||||
icon: mdi:tune
|
||||
unit_of_measurement: °C
|
||||
mode: box
|
||||
fake_opening_degree2:
|
||||
name: Sonoff Opening degree 2
|
||||
min: 0
|
||||
max: 100
|
||||
icon: mdi:valve-open
|
||||
unit_of_measurement: "%"
|
||||
mode: box
|
||||
fake_closing_degree2:
|
||||
name: Sonoff Closing degree 2
|
||||
min: 0
|
||||
max: 100
|
||||
icon: mdi:valve-closed
|
||||
unit_of_measurement: "%"
|
||||
mode: box
|
||||
|
||||
input_boolean:
|
||||
# input_boolean to simulate the windows entity. Only for development environment.
|
||||
@@ -142,6 +184,12 @@ input_boolean:
|
||||
fake_presence_sensor1:
|
||||
name: Presence Sensor 1
|
||||
icon: mdi:home
|
||||
fake_valve_sonoff_trvzb1:
|
||||
name: Valve Sonoff TRVZB1
|
||||
icon: mdi:valve
|
||||
fake_valve_sonoff_trvzb2:
|
||||
name: Valve Sonoff TRVZB2
|
||||
icon: mdi:valve
|
||||
|
||||
climate:
|
||||
- platform: generic_thermostat
|
||||
@@ -185,6 +233,16 @@ climate:
|
||||
name: Underlying thermostat9
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
- platform: generic_thermostat
|
||||
name: Underlying Sonoff TRVZB1
|
||||
heater: input_boolean.fake_valve_sonoff_trvzb1
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying Sonoff TRVZB2
|
||||
heater: input_boolean.fake_valve_sonoff_trvzb2
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
|
||||
input_datetime:
|
||||
fake_last_seen:
|
||||
@@ -238,14 +296,14 @@ switch:
|
||||
friendly_name: "Pilote chauffage SDB RDC"
|
||||
value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}"
|
||||
turn_on:
|
||||
service: select.select_option
|
||||
action: select.select_option
|
||||
data:
|
||||
option: comfort
|
||||
target:
|
||||
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
|
||||
|
||||
turn_off:
|
||||
service: select.select_option
|
||||
action: select.select_option
|
||||
data:
|
||||
option: comfort-2
|
||||
target:
|
||||
|
||||
1699
README-fr.md
@@ -57,10 +57,13 @@ class AutoStartStopDetectionAlgorithm:
|
||||
_accumulated_error: float = 0
|
||||
_error_threshold: float | None = None
|
||||
_last_calculation_date: datetime | None = None
|
||||
_last_switch_date: datetime | None = None
|
||||
|
||||
def __init__(self, level: TYPE_AUTO_START_STOP_LEVELS, vtherm_name) -> None:
|
||||
"""Initalize a new algorithm with the right constants"""
|
||||
self._vtherm_name = vtherm_name
|
||||
self._last_calculation_date = None
|
||||
self._last_switch_date = None
|
||||
self._init_level(level)
|
||||
|
||||
def _init_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
|
||||
@@ -143,17 +146,26 @@ class AutoStartStopDetectionAlgorithm:
|
||||
|
||||
temp_at_dt = current_temp + slope_min * self._dt
|
||||
|
||||
# Calculate the number of minute from last_switch
|
||||
nb_minutes_since_last_switch = 999
|
||||
if self._last_switch_date is not None:
|
||||
nb_minutes_since_last_switch = (
|
||||
now - self._last_switch_date
|
||||
).total_seconds() / 60
|
||||
|
||||
# Check to turn-off
|
||||
# When we hit the threshold, that mean we can turn off
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
if (
|
||||
self._accumulated_error <= -self._error_threshold
|
||||
and temp_at_dt >= target_temp + TEMP_HYSTERESIS
|
||||
and nb_minutes_since_last_switch >= self._dt
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to stop, there is no need for heating for a long time.",
|
||||
self,
|
||||
)
|
||||
self._last_switch_date = now
|
||||
return AUTO_START_STOP_ACTION_OFF
|
||||
else:
|
||||
_LOGGER.debug("%s - nothing to do, we are heating", self)
|
||||
@@ -163,11 +175,13 @@ class AutoStartStopDetectionAlgorithm:
|
||||
if (
|
||||
self._accumulated_error >= self._error_threshold
|
||||
and temp_at_dt <= target_temp - TEMP_HYSTERESIS
|
||||
and nb_minutes_since_last_switch >= self._dt
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to stop, there is no need for cooling for a long time.",
|
||||
self,
|
||||
)
|
||||
self._last_switch_date = now
|
||||
return AUTO_START_STOP_ACTION_OFF
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
@@ -178,11 +192,15 @@ class AutoStartStopDetectionAlgorithm:
|
||||
|
||||
# check to turn on
|
||||
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT:
|
||||
if temp_at_dt <= target_temp - TEMP_HYSTERESIS:
|
||||
if (
|
||||
temp_at_dt <= target_temp - TEMP_HYSTERESIS
|
||||
and nb_minutes_since_last_switch >= self._dt
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to start, because it will be time to heat",
|
||||
self,
|
||||
)
|
||||
self._last_switch_date = now
|
||||
return AUTO_START_STOP_ACTION_ON
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
@@ -192,11 +210,15 @@ class AutoStartStopDetectionAlgorithm:
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL:
|
||||
if temp_at_dt >= target_temp + TEMP_HYSTERESIS:
|
||||
if (
|
||||
temp_at_dt >= target_temp + TEMP_HYSTERESIS
|
||||
and nb_minutes_since_last_switch >= self._dt
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - We need to start, because it will be time to cool",
|
||||
self,
|
||||
)
|
||||
self._last_switch_date = now
|
||||
return AUTO_START_STOP_ACTION_ON
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
@@ -235,5 +257,10 @@ class AutoStartStopDetectionAlgorithm:
|
||||
"""Get the level value"""
|
||||
return self._level
|
||||
|
||||
@property
|
||||
def last_switch_date(self) -> datetime | None:
|
||||
"""Get the last of the last switch"""
|
||||
return self._last_switch_date
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}"
|
||||
|
||||
@@ -9,7 +9,6 @@ from datetime import timedelta, datetime
|
||||
from types import MappingProxyType
|
||||
from typing import Any, TypeVar, Generic
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
@@ -80,17 +79,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ConfigData = MappingProxyType[str, Any]
|
||||
T = TypeVar("T", bound=UnderlyingEntity)
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
"""Get the current timezone"""
|
||||
|
||||
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."""
|
||||
|
||||
@@ -139,10 +127,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"max_power_sensor_entity_id",
|
||||
"temperature_unit",
|
||||
"is_device_active",
|
||||
"nb_device_actives",
|
||||
"target_temperature_step",
|
||||
"is_used_by_central_boiler",
|
||||
"temperature_slope",
|
||||
"max_on_percent"
|
||||
"max_on_percent",
|
||||
"have_valve_regulation",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -206,7 +196,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)
|
||||
_LOGGER.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
|
||||
@@ -464,8 +454,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
else DEFAULT_SECURITY_DEFAULT_ON_PERCENT
|
||||
)
|
||||
self._minimal_activation_delay = entry_infos.get(CONF_MINIMAL_ACTIVATION_DELAY)
|
||||
self._last_temperature_measure = datetime.now(tz=self._current_tz)
|
||||
self._last_ext_temperature_measure = datetime.now(tz=self._current_tz)
|
||||
self._last_temperature_measure = self.now
|
||||
self._last_ext_temperature_measure = self.now
|
||||
self._security_state = False
|
||||
|
||||
# Initiate the ProportionalAlgorithm
|
||||
@@ -479,7 +469,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)
|
||||
_LOGGER.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
|
||||
@@ -508,7 +498,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
entry_infos.get(CONF_WINDOW_ACTION) or CONF_WINDOW_TURN_OFF
|
||||
)
|
||||
|
||||
self._max_on_percent = api._max_on_percent
|
||||
self._max_on_percent = api.max_on_percent
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Creation of a new VersatileThermostat entity: unique_id=%s",
|
||||
@@ -599,7 +589,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Try to force backup of entity"""
|
||||
_LOGGER_ENERGY.debug(
|
||||
_LOGGER.debug(
|
||||
"%s - force write before remove. Energy is %s", self, self.total_energy
|
||||
)
|
||||
# Force dump in background
|
||||
@@ -826,7 +816,7 @@ 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(
|
||||
_LOGGER.debug(
|
||||
"%s - get_my_previous_state restored energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
@@ -844,7 +834,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"No previously saved temperature, setting to %s", self._target_temp
|
||||
)
|
||||
self._total_energy = 0
|
||||
_LOGGER_ENERGY.debug(
|
||||
_LOGGER.debug(
|
||||
"%s - get_my_previous_state no previous state energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
@@ -1006,6 +996,15 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def nb_device_actives(self) -> int:
|
||||
"""Calculate the number of active devices"""
|
||||
ret = 0
|
||||
for under in self._underlyings:
|
||||
if under.is_device_active:
|
||||
ret += 1
|
||||
return ret
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the sensor temperature."""
|
||||
@@ -1133,6 +1132,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"""Returns the underlying entities"""
|
||||
return self._underlyings
|
||||
|
||||
@property
|
||||
def activable_underlying_entities(self) -> list | None:
|
||||
"""Returns the activable underlying entities for controling the central boiler"""
|
||||
return self.underlying_entities
|
||||
|
||||
def find_underlying_by_entity_id(self, entity_id: str) -> Entity | None:
|
||||
"""Get the underlying entity by a entity_id"""
|
||||
for under in self._underlyings:
|
||||
@@ -1325,8 +1329,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self._attr_preset_mode = PRESET_ACTIVITY
|
||||
await self._async_update_motion_temp()
|
||||
else:
|
||||
if self._attr_preset_mode == PRESET_NONE:
|
||||
self._saved_target_temp = self._target_temp
|
||||
# if self._attr_preset_mode == PRESET_NONE:
|
||||
# self._saved_target_temp = self._target_temp
|
||||
self._attr_preset_mode = preset_mode
|
||||
await self._async_internal_set_temperature(
|
||||
self.find_preset_temp(preset_mode)
|
||||
@@ -1346,7 +1350,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self, old_preset_mode: str | None = None
|
||||
): # pylint: disable=unused-argument
|
||||
"""Reset to now the last change time"""
|
||||
self._last_change_time = datetime.now(tz=self._current_tz)
|
||||
self._last_change_time = self.now
|
||||
_LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time)
|
||||
|
||||
def reset_last_temperature_time(self, old_preset_mode: str | None = None):
|
||||
@@ -1356,7 +1360,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
and old_preset_mode not in HIDDEN_PRESETS
|
||||
):
|
||||
self._last_temperature_measure = self._last_ext_temperature_measure = (
|
||||
datetime.now(tz=self._current_tz)
|
||||
self.now
|
||||
)
|
||||
|
||||
def find_preset_temp(self, preset_mode: str):
|
||||
@@ -1389,7 +1393,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
)
|
||||
|
||||
if motion_preset in self._presets:
|
||||
return self._presets[motion_preset]
|
||||
if self._presence_on and self.presence_state in [STATE_OFF, None]:
|
||||
return self._presets_away[motion_preset + PRESET_AWAY_SUFFIX]
|
||||
else:
|
||||
return self._presets[motion_preset]
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
@@ -1459,16 +1466,16 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"""Extract the last_changed state from State or return now if not available"""
|
||||
return (
|
||||
state.last_changed.astimezone(self._current_tz)
|
||||
if state.last_changed is not None
|
||||
else datetime.now(tz=self._current_tz)
|
||||
if isinstance(state.last_changed, datetime)
|
||||
else self.now
|
||||
)
|
||||
|
||||
def get_last_updated_date_or_now(self, state: State) -> datetime:
|
||||
"""Extract the last_changed state from State or return now if not available"""
|
||||
return (
|
||||
state.last_updated.astimezone(self._current_tz)
|
||||
if state.last_updated is not None
|
||||
else datetime.now(tz=self._current_tz)
|
||||
if isinstance(state.last_updated, datetime)
|
||||
else self.now
|
||||
)
|
||||
|
||||
@callback
|
||||
@@ -1910,7 +1917,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
STATE_NOT_HOME,
|
||||
):
|
||||
return
|
||||
if self._attr_preset_mode not in [PRESET_BOOST, PRESET_COMFORT, PRESET_ECO]:
|
||||
if self._attr_preset_mode not in [
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
PRESET_ACTIVITY,
|
||||
]:
|
||||
return
|
||||
|
||||
new_temp = self.find_preset_temp(self.preset_mode)
|
||||
@@ -2000,7 +2012,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
if in_cycle:
|
||||
slope = self._window_auto_algo.check_age_last_measurement(
|
||||
temperature=self._ema_temp,
|
||||
datetime_now=datetime.now(get_tz(self._hass)),
|
||||
datetime_now=self.now,
|
||||
)
|
||||
else:
|
||||
slope = self._window_auto_algo.add_temp_measurement(
|
||||
@@ -2288,10 +2300,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
@property
|
||||
def now(self) -> datetime:
|
||||
"""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)
|
||||
return self._now if self._now is not None else NowClass.get_now(self._hass)
|
||||
|
||||
async def check_safety(self) -> bool:
|
||||
"""Check if last temperature date is too long"""
|
||||
|
||||
now = self.now
|
||||
delta_temp = (
|
||||
now - self._last_temperature_measure.replace(tzinfo=self._current_tz)
|
||||
@@ -2487,7 +2500,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF
|
||||
"%s - Window is open. Apply window action %s", self, self._window_action
|
||||
)
|
||||
if self._window_action == CONF_WINDOW_TURN_OFF and not self.is_on:
|
||||
_LOGGER.debug(
|
||||
@@ -2659,20 +2672,20 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"device_power": self._device_power,
|
||||
ATTR_MEAN_POWER_CYCLE: self.mean_cycle_power,
|
||||
ATTR_TOTAL_ENERGY: self.total_energy,
|
||||
"last_update_datetime": datetime.now()
|
||||
.astimezone(self._current_tz)
|
||||
.isoformat(),
|
||||
"last_update_datetime": self.now.isoformat(),
|
||||
"timezone": str(self._current_tz),
|
||||
"temperature_unit": self.temperature_unit,
|
||||
"is_device_active": self.is_device_active,
|
||||
"nb_device_actives": self.nb_device_actives,
|
||||
"ema_temp": self._ema_temp,
|
||||
"is_used_by_central_boiler": self.is_used_by_central_boiler,
|
||||
"temperature_slope": round(self.last_temperature_slope or 0, 3),
|
||||
"hvac_off_reason": self.hvac_off_reason,
|
||||
"max_on_percent": self._max_on_percent,
|
||||
"have_valve_regulation": self.have_valve_regulation,
|
||||
}
|
||||
|
||||
_LOGGER_ENERGY.debug(
|
||||
_LOGGER.debug(
|
||||
"%s - update_custom_attributes saved energy is %s",
|
||||
self,
|
||||
self.total_energy,
|
||||
@@ -2681,13 +2694,18 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
@overrides
|
||||
def async_write_ha_state(self):
|
||||
"""overrides to have log"""
|
||||
_LOGGER_ENERGY.debug(
|
||||
_LOGGER.debug(
|
||||
"%s - async_write_ha_state written state energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
return super().async_write_ha_state()
|
||||
|
||||
@property
|
||||
def have_valve_regulation(self) -> bool:
|
||||
"""True if the Thermostat is regulated by valve"""
|
||||
return False
|
||||
|
||||
@callback
|
||||
def async_registry_entry_updated(self):
|
||||
"""update the entity if the config entry have been updated
|
||||
|
||||
@@ -108,7 +108,7 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_is_on
|
||||
self._attr_is_on = self.my_climate.security_state is True
|
||||
@@ -147,7 +147,7 @@ class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_is_on
|
||||
self._attr_is_on = self.my_climate.overpowering_state is True
|
||||
@@ -186,7 +186,7 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_is_on
|
||||
# Issue 120 - only take defined presence value
|
||||
@@ -236,7 +236,7 @@ class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
old_state = self._attr_is_on
|
||||
# Issue 120 - only take defined presence value
|
||||
if self.my_climate.motion_state in [STATE_ON, STATE_OFF]:
|
||||
@@ -277,7 +277,7 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
old_state = self._attr_is_on
|
||||
# Issue 120 - only take defined presence value
|
||||
if self.my_climate.presence_state in [STATE_ON, STATE_OFF]:
|
||||
@@ -317,7 +317,7 @@ class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
old_state = self._attr_is_on
|
||||
if self.my_climate.window_bypass_state in [True, False]:
|
||||
self._attr_is_on = self.my_climate.window_bypass_state
|
||||
|
||||
@@ -22,26 +22,12 @@ from homeassistant.const import (
|
||||
STATE_NOT_HOME,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
CONF_PRESETS_WITH_AC,
|
||||
SERVICE_SET_PRESENCE,
|
||||
SERVICE_SET_PRESET_TEMPERATURE,
|
||||
SERVICE_SET_SECURITY,
|
||||
SERVICE_SET_WINDOW_BYPASS,
|
||||
SERVICE_SET_AUTO_REGULATION_MODE,
|
||||
SERVICE_SET_AUTO_FAN_MODE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
)
|
||||
from .const import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
from .thermostat_switch import ThermostatOverSwitch
|
||||
from .thermostat_climate import ThermostatOverClimate
|
||||
from .thermostat_valve import ThermostatOverValve
|
||||
from .thermostat_climate_valve import ThermostatOverClimateValve
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -60,6 +46,9 @@ async def async_setup_entry(
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
have_valve_regulation = (
|
||||
entry.data.get(CONF_AUTO_REGULATION_MODE) == CONF_AUTO_REGULATION_VALVE
|
||||
)
|
||||
|
||||
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
return
|
||||
@@ -69,7 +58,10 @@ async def async_setup_entry(
|
||||
if vt_type == CONF_THERMOSTAT_SWITCH:
|
||||
entity = ThermostatOverSwitch(hass, unique_id, name, entry.data)
|
||||
elif vt_type == CONF_THERMOSTAT_CLIMATE:
|
||||
entity = ThermostatOverClimate(hass, unique_id, name, entry.data)
|
||||
if have_valve_regulation is True:
|
||||
entity = ThermostatOverClimateValve(hass, unique_id, name, entry.data)
|
||||
else:
|
||||
entity = ThermostatOverClimate(hass, unique_id, name, entry.data)
|
||||
elif vt_type == CONF_THERMOSTAT_VALVE:
|
||||
entity = ThermostatOverValve(hass, unique_id, name, entry.data)
|
||||
else:
|
||||
|
||||
@@ -3,38 +3,20 @@
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
from datetime import timedelta
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_call_later
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
"""Get the current timezone"""
|
||||
|
||||
return dt_util.get_time_zone(hass.config.time_zone)
|
||||
|
||||
|
||||
class NowClass:
|
||||
"""For testing purpose only"""
|
||||
|
||||
@staticmethod
|
||||
def get_now(hass: HomeAssistant) -> datetime:
|
||||
"""A test function to get the now.
|
||||
For testing purpose this method can be overriden to get a specific
|
||||
timestamp.
|
||||
"""
|
||||
return datetime.now(get_tz(hass))
|
||||
|
||||
|
||||
def round_to_nearest(n: float, x: float) -> float:
|
||||
"""Round a number to the nearest x (which should be decimal but not null)
|
||||
Example:
|
||||
|
||||
@@ -29,27 +29,6 @@ COMES_FROM = "comes_from"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Not used but can be useful in other context
|
||||
# def schema_defaults(schema, **defaults):
|
||||
# """Create a new schema with default values filled in."""
|
||||
# copy = schema.extend({})
|
||||
# for field, field_type in copy.schema.items():
|
||||
# if isinstance(field_type, vol.In):
|
||||
# value = None
|
||||
#
|
||||
# if value in field_type.container:
|
||||
# # field.default = vol.default_factory(value)
|
||||
# field.description = {"suggested_value": value}
|
||||
# continue
|
||||
#
|
||||
# if field.schema in defaults:
|
||||
# # field.default = vol.default_factory(defaults[field])
|
||||
# field.description = {"suggested_value": defaults[field]}
|
||||
# return copy
|
||||
#
|
||||
|
||||
|
||||
def add_suggested_values_to_schema(
|
||||
data_schema: vol.Schema, suggested_values: Mapping[str, Any]
|
||||
) -> vol.Schema:
|
||||
@@ -77,7 +56,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
VERSION = CONFIG_VERSION
|
||||
MINOR_VERSION = CONFIG_MINOR_VERSION
|
||||
|
||||
_infos: dict
|
||||
_placeholders = {
|
||||
CONF_NAME: "",
|
||||
}
|
||||
@@ -85,7 +63,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
def __init__(self, infos) -> None:
|
||||
super().__init__()
|
||||
_LOGGER.debug("CTOR BaseConfigFlow infos: %s", infos)
|
||||
self._infos = infos
|
||||
self._infos: dict = infos
|
||||
|
||||
# VTherm API should have been initialized before arriving here
|
||||
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||
@@ -94,8 +72,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
else:
|
||||
self._central_config = None
|
||||
|
||||
self._init_feature_flags(infos)
|
||||
self._init_central_config_flags(infos)
|
||||
self._init_feature_flags(infos)
|
||||
|
||||
def _init_feature_flags(self, _):
|
||||
"""Fix features selection depending to infos"""
|
||||
@@ -162,7 +140,45 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
if COMES_FROM in self._infos:
|
||||
del self._infos[COMES_FROM]
|
||||
|
||||
async def validate_input(self, data: dict) -> None:
|
||||
def is_valve_regulation_selected(self, infos) -> bool:
|
||||
"""True of the valve regulation mode is selected"""
|
||||
return infos.get(CONF_AUTO_REGULATION_MODE, None) == CONF_AUTO_REGULATION_VALVE
|
||||
|
||||
def check_valve_regulation_nb_entities(self, data: dict, step_id=None) -> bool:
|
||||
"""Check the number of entities for Valve regulation"""
|
||||
if step_id not in ["type", "valve_regulation", "check_complete"]:
|
||||
return True
|
||||
|
||||
underlyings_to_check = data if step_id == "type" else self._infos
|
||||
# underlyings_to_check = self._infos # data if step_id == "type" else self._infos
|
||||
regulation_infos_to_check = (
|
||||
data if step_id == "valve_regulation" else self._infos
|
||||
)
|
||||
|
||||
ret = True
|
||||
if (
|
||||
self.is_valve_regulation_selected(underlyings_to_check)
|
||||
and step_id != "type"
|
||||
):
|
||||
nb_unders = len(underlyings_to_check.get(CONF_UNDERLYING_LIST))
|
||||
nb_offset = len(
|
||||
regulation_infos_to_check.get(CONF_OFFSET_CALIBRATION_LIST, [])
|
||||
)
|
||||
nb_opening = len(
|
||||
regulation_infos_to_check.get(CONF_OPENING_DEGREE_LIST, [])
|
||||
)
|
||||
nb_closing = len(
|
||||
regulation_infos_to_check.get(CONF_CLOSING_DEGREE_LIST, [])
|
||||
)
|
||||
if (
|
||||
nb_unders != nb_opening
|
||||
or (nb_unders != nb_offset and nb_offset > 0)
|
||||
or (nb_unders != nb_closing and nb_closing > 0)
|
||||
):
|
||||
ret = False
|
||||
return ret
|
||||
|
||||
async def validate_input(self, data: dict, step_id) -> None:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from STEP_*_DATA_SCHEMA with values provided by the user.
|
||||
@@ -178,6 +194,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
CONF_OFFSET_CALIBRATION_LIST,
|
||||
CONF_OPENING_DEGREE_LIST,
|
||||
CONF_CLOSING_DEGREE_LIST,
|
||||
]:
|
||||
d = data.get(conf, None) # pylint: disable=invalid-name
|
||||
if not isinstance(d, list):
|
||||
@@ -235,6 +254,11 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
except ServiceConfigurationError as err:
|
||||
raise ServiceConfigurationError(conf) from err
|
||||
|
||||
# Check that the number of offet_calibration and opening_degree and closing_degree are equals
|
||||
# to the number of underlying entities
|
||||
if not self.check_valve_regulation_nb_entities(data, step_id):
|
||||
raise ValveRegulationNbEntitiesIncorrect()
|
||||
|
||||
def check_config_complete(self, infos) -> bool:
|
||||
"""True if the config is now complete (ie all mandatory attributes are set)"""
|
||||
is_central_config = (
|
||||
@@ -330,6 +354,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
):
|
||||
return False
|
||||
|
||||
if not self.check_valve_regulation_nb_entities(infos, "check_complete"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def merge_user_input(self, data_schema: vol.Schema, user_input: dict):
|
||||
@@ -359,7 +386,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
if user_input is not None:
|
||||
defaults.update(user_input or {})
|
||||
try:
|
||||
await self.validate_input(user_input)
|
||||
await self.validate_input(user_input, step_id)
|
||||
except UnknownEntity as err:
|
||||
errors[str(err)] = "unknown_entity"
|
||||
except WindowOpenDetectionMethod as err:
|
||||
@@ -370,6 +397,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
errors[str(err)] = "service_configuration_format"
|
||||
except ConfigurationNotCompleteError as err:
|
||||
errors["base"] = "configuration_not_complete"
|
||||
except ValveRegulationNbEntitiesIncorrect as err:
|
||||
errors["base"] = "valve_regulation_nb_entities_incorrect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -421,6 +450,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
if (
|
||||
self._infos.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI
|
||||
or is_central_config
|
||||
or self.is_valve_regulation_selected(self._infos)
|
||||
):
|
||||
menu_options.append("tpi")
|
||||
|
||||
@@ -456,6 +486,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
]:
|
||||
menu_options.append("auto_start_stop")
|
||||
|
||||
if self.is_valve_regulation_selected(self._infos):
|
||||
menu_options.append("valve_regulation")
|
||||
|
||||
menu_options.append("advanced")
|
||||
|
||||
if self.check_config_complete(self._infos):
|
||||
@@ -525,6 +558,23 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"""Handle the Type flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_type user_input=%s", user_input)
|
||||
|
||||
if (
|
||||
self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CLIMATE
|
||||
and user_input is not None
|
||||
and not self.is_valve_regulation_selected(user_input)
|
||||
):
|
||||
# Remove TPI info
|
||||
for key in [
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_TPI_COEF_INT,
|
||||
CONF_TPI_COEF_EXT,
|
||||
CONF_OFFSET_CALIBRATION_LIST,
|
||||
CONF_OPENING_DEGREE_LIST,
|
||||
CONF_CLOSING_DEGREE_LIST,
|
||||
]:
|
||||
if self._infos.get(key):
|
||||
del self._infos[key]
|
||||
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_SWITCH:
|
||||
return await self.generic_step(
|
||||
"type", STEP_THERMOSTAT_SWITCH, user_input, self.async_step_menu
|
||||
@@ -568,6 +618,22 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
|
||||
return await self.generic_step("auto_start_stop", schema, user_input, next_step)
|
||||
|
||||
async def async_step_valve_regulation(
|
||||
self, user_input: dict | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the valve regulation configuration step"""
|
||||
_LOGGER.debug(
|
||||
"Into ConfigFlow.async_step_valve_regulation user_input=%s", user_input
|
||||
)
|
||||
|
||||
schema = STEP_VALVE_REGULATION
|
||||
self._infos[COMES_FROM] = None
|
||||
next_step = self.async_step_menu
|
||||
|
||||
return await self.generic_step(
|
||||
"valve_regulation", schema, user_input, next_step
|
||||
)
|
||||
|
||||
async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the TPI flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input)
|
||||
|
||||
@@ -197,6 +197,31 @@ STEP_AUTO_START_STOP = vol.Schema( # pylint: disable=invalid-name
|
||||
}
|
||||
)
|
||||
|
||||
STEP_VALVE_REGULATION = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_OPENING_DEGREE_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_OFFSET_CALIBRATION_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_CLOSING_DEGREE_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
|
||||
[
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_USE_TPI_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||
@@ -205,8 +230,16 @@ STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
|
||||
STEP_CENTRAL_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_TPI_COEF_INT, default=0.6): vol.Coerce(float),
|
||||
vol.Required(CONF_TPI_COEF_EXT, default=0.01): vol.Coerce(float),
|
||||
vol.Required(CONF_TPI_COEF_INT, default=0.6): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0.0, max=1.0, step=0.01, mode=selector.NumberSelectorMode.BOX
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_TPI_COEF_EXT, default=0.01): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0.0, max=1.0, step=0.01, mode=selector.NumberSelectorMode.BOX
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
"""Constants for the Versatile Thermostat integration."""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Literal
|
||||
from datetime import datetime
|
||||
|
||||
from enum import Enum
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
@@ -16,6 +19,7 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .prop_algorithm import (
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
@@ -99,6 +103,7 @@ CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
|
||||
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
|
||||
CONF_AUTO_REGULATION_MODE = "auto_regulation_mode"
|
||||
CONF_AUTO_REGULATION_NONE = "auto_regulation_none"
|
||||
CONF_AUTO_REGULATION_VALVE = "auto_regulation_valve"
|
||||
CONF_AUTO_REGULATION_SLOW = "auto_regulation_slow"
|
||||
CONF_AUTO_REGULATION_LIGHT = "auto_regulation_light"
|
||||
CONF_AUTO_REGULATION_MEDIUM = "auto_regulation_medium"
|
||||
@@ -115,6 +120,9 @@ CONF_AUTO_FAN_MEDIUM = "auto_fan_medium"
|
||||
CONF_AUTO_FAN_HIGH = "auto_fan_high"
|
||||
CONF_AUTO_FAN_TURBO = "auto_fan_turbo"
|
||||
CONF_STEP_TEMPERATURE = "step_temperature"
|
||||
CONF_OFFSET_CALIBRATION_LIST = "offset_calibration_entity_ids"
|
||||
CONF_OPENING_DEGREE_LIST = "opening_degree_entity_ids"
|
||||
CONF_CLOSING_DEGREE_LIST = "closing_degree_entity_ids"
|
||||
|
||||
# Deprecated
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
@@ -321,6 +329,7 @@ CONF_FUNCTIONS = [
|
||||
|
||||
CONF_AUTO_REGULATION_MODES = [
|
||||
CONF_AUTO_REGULATION_NONE,
|
||||
CONF_AUTO_REGULATION_VALVE,
|
||||
CONF_AUTO_REGULATION_LIGHT,
|
||||
CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_STRONG,
|
||||
@@ -459,9 +468,9 @@ class RegulationParamVeryStrong:
|
||||
kp: float = 0.6
|
||||
ki: float = 0.1
|
||||
k_ext: float = 0.2
|
||||
offset_max: float = 4
|
||||
offset_max: float = 8
|
||||
stabilization_threshold: float = 0.1
|
||||
accumulated_error_threshold: float = 30
|
||||
accumulated_error_threshold: float = 80
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
@@ -486,6 +495,38 @@ def send_vtherm_event(hass, event_type: EventType, entity, data: dict):
|
||||
hass.bus.fire(event_type.value, data)
|
||||
|
||||
|
||||
def get_safe_float(hass, entity_id: str):
|
||||
"""Get a safe float state value for an entity.
|
||||
Return None if entity is not available"""
|
||||
if (
|
||||
entity_id is None
|
||||
or not (state := hass.states.get(entity_id))
|
||||
or state.state == "unknown"
|
||||
or state.state == "unavailable"
|
||||
):
|
||||
return None
|
||||
float_val = float(state.state)
|
||||
return None if math.isinf(float_val) or not math.isfinite(float_val) else float_val
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
"""Get the current timezone"""
|
||||
|
||||
return dt_util.get_time_zone(hass.config.time_zone)
|
||||
|
||||
|
||||
class NowClass:
|
||||
"""For testing purpose only"""
|
||||
|
||||
@staticmethod
|
||||
def get_now(hass: HomeAssistant) -> datetime:
|
||||
"""A test function to get the now.
|
||||
For testing purpose this method can be overriden to get a specific
|
||||
timestamp.
|
||||
"""
|
||||
return datetime.now(get_tz(hass))
|
||||
|
||||
|
||||
class UnknownEntity(HomeAssistantError):
|
||||
"""Error to indicate there is an unknown entity_id given."""
|
||||
|
||||
@@ -506,6 +547,11 @@ class ConfigurationNotCompleteError(HomeAssistantError):
|
||||
"""Error the configuration is not complete"""
|
||||
|
||||
|
||||
class ValveRegulationNbEntitiesIncorrect(HomeAssistantError):
|
||||
"""Error to indicate there is an error in the configuration of the TRV with valve regulation.
|
||||
The number of specific entities is incorrect."""
|
||||
|
||||
|
||||
class overrides: # pylint: disable=invalid-name
|
||||
"""An annotation to inform overrides"""
|
||||
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "6.7.0",
|
||||
"version": "6.8.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -26,20 +26,14 @@ MIN_NB_POINT = 4 # do not calculate slope until we have enough point
|
||||
class WindowOpenDetectionAlgorithm:
|
||||
"""The class that implements the algorithm listed above"""
|
||||
|
||||
_alert_threshold: float
|
||||
_end_alert_threshold: float
|
||||
_last_slope: float
|
||||
_last_datetime: datetime
|
||||
_last_temperature: float
|
||||
_nb_point: int
|
||||
|
||||
def __init__(self, alert_threshold, end_alert_threshold) -> None:
|
||||
"""Initalize a new algorithm with the both threshold"""
|
||||
self._alert_threshold = alert_threshold
|
||||
self._end_alert_threshold = end_alert_threshold
|
||||
self._last_slope = None
|
||||
self._last_datetime = None
|
||||
self._nb_point = 0
|
||||
self._alert_threshold: float = alert_threshold
|
||||
self._end_alert_threshold: float = end_alert_threshold
|
||||
self._last_slope: float | None = None
|
||||
self._last_datetime: datetime = None
|
||||
self._last_temperature: float | None = None
|
||||
self._nb_point: int = 0
|
||||
|
||||
def check_age_last_measurement(self, temperature, datetime_now) -> float:
|
||||
""" " Check if last measurement is old and add
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
import math
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback, Event, CoreState
|
||||
from homeassistant.core import HomeAssistant, callback, Event, CoreState, State
|
||||
|
||||
from homeassistant.const import (
|
||||
UnitOfTime,
|
||||
@@ -17,20 +17,19 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
|
||||
|
||||
@@ -50,6 +49,8 @@ from .const import (
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
CONF_AUTO_REGULATION_VALVE,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
overrides,
|
||||
)
|
||||
|
||||
@@ -71,6 +72,9 @@ async def async_setup_entry(
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
have_valve_regulation = (
|
||||
entry.data.get(CONF_AUTO_REGULATION_MODE) == CONF_AUTO_REGULATION_VALVE
|
||||
)
|
||||
|
||||
entities = None
|
||||
|
||||
@@ -99,10 +103,16 @@ async def async_setup_entry(
|
||||
entities.append(OnTimeSensor(hass, unique_id, name, entry.data))
|
||||
entities.append(OffTimeSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE:
|
||||
if (
|
||||
entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE
|
||||
or have_valve_regulation
|
||||
):
|
||||
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE:
|
||||
if (
|
||||
entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
|
||||
and not have_valve_regulation
|
||||
):
|
||||
entities.append(
|
||||
RegulatedTemperatureSensor(hass, unique_id, name, entry.data)
|
||||
)
|
||||
@@ -123,7 +133,7 @@ class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
energy = self.my_climate.total_energy
|
||||
if energy is None:
|
||||
@@ -178,7 +188,7 @@ class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
if math.isnan(float(self.my_climate.mean_cycle_power)) or math.isinf(
|
||||
self.my_climate.mean_cycle_power
|
||||
@@ -235,7 +245,7 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
on_percent = (
|
||||
float(self.my_climate.proportional_algorithm.on_percent)
|
||||
@@ -290,7 +300,7 @@ class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = self.my_climate.valve_open_percent
|
||||
@@ -332,7 +342,7 @@ class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
on_time = (
|
||||
float(self.my_climate.proportional_algorithm.on_time_sec)
|
||||
@@ -381,7 +391,7 @@ class OffTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
off_time = (
|
||||
float(self.my_climate.proportional_algorithm.off_time_sec)
|
||||
@@ -429,7 +439,7 @@ class LastTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = self.my_climate.last_temperature_measure
|
||||
@@ -458,7 +468,7 @@ class LastExtTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = self.my_climate.last_ext_temperature_measure
|
||||
@@ -487,7 +497,7 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
last_slope = self.my_climate.last_temperature_slope
|
||||
if last_slope is None:
|
||||
@@ -540,7 +550,7 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
new_temp = self.my_climate.regulated_target_temp
|
||||
if new_temp is None:
|
||||
@@ -591,7 +601,7 @@ class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
new_ema = self.my_climate.ema_temperature
|
||||
if new_ema is None:
|
||||
@@ -698,7 +708,7 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
|
||||
for entity in component.entities:
|
||||
if isinstance(entity, BaseThermostat) and entity.is_used_by_central_boiler:
|
||||
self._entities.append(entity)
|
||||
for under in entity.underlying_entities:
|
||||
for under in entity.activable_underlying_entities:
|
||||
underlying_entities_id.append(under.entity_id)
|
||||
if len(underlying_entities_id) > 0:
|
||||
# Arme l'écoute de la première entité
|
||||
@@ -718,25 +728,65 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
|
||||
|
||||
await self.calculate_nb_active_devices(None)
|
||||
|
||||
async def calculate_nb_active_devices(self, _):
|
||||
async def calculate_nb_active_devices(self, event: Event):
|
||||
"""Calculate the number of active VTherm that have an
|
||||
influence on central boiler"""
|
||||
|
||||
_LOGGER.debug("%s - calculating the number of active VTherm", self)
|
||||
# _LOGGER.debug("%s- calculate_nb_active_devices - the event is %s ", self, event)
|
||||
|
||||
if event is not None:
|
||||
new_state: State = event.data.get("new_state")
|
||||
# _LOGGER.debug(
|
||||
# "%s - calculate_nb_active_devices new_state is %s", self, new_state
|
||||
# )
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
old_state: State = event.data.get("old_state")
|
||||
|
||||
# For underlying climate, we need to observe also the hvac_action if available
|
||||
new_hvac_action = new_state.attributes.get("hvac_action")
|
||||
old_hvac_action = (
|
||||
old_state.attributes.get("hvac_action")
|
||||
if old_state is not None
|
||||
else None
|
||||
)
|
||||
|
||||
# Filter events that are not interested for us
|
||||
if (
|
||||
old_state is not None
|
||||
and new_state.state == old_state.state
|
||||
and new_hvac_action == old_hvac_action
|
||||
):
|
||||
# A false state change
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - calculating the number of active underlying device for boiler activation. change change from %s to %s",
|
||||
self,
|
||||
old_state,
|
||||
new_state,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - calculating the number of active underlying device for boiler activation. First time calculation",
|
||||
self,
|
||||
)
|
||||
|
||||
nb_active = 0
|
||||
for entity in self._entities:
|
||||
nb_active += entity.nb_device_actives
|
||||
_LOGGER.debug(
|
||||
"Examining the hvac_action of %s",
|
||||
"After examining the hvac_action of %s, nb_active is %s",
|
||||
entity.name,
|
||||
nb_active,
|
||||
)
|
||||
if (
|
||||
entity.hvac_mode in [HVACMode.HEAT, HVACMode.AUTO]
|
||||
and entity.hvac_action == HVACAction.HEATING
|
||||
):
|
||||
for under in entity.underlying_entities:
|
||||
nb_active += 1 if under.is_device_active else 0
|
||||
|
||||
self._attr_native_value = nb_active
|
||||
_LOGGER.debug(
|
||||
"%s - Number of active underlying entities is %s", self, nb_active
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"valve_regulation": "Valve regulation configuration",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
@@ -64,7 +65,7 @@
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
@@ -203,6 +204,34 @@
|
||||
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Control of the central boiler",
|
||||
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Command to turn-on",
|
||||
"central_boiler_deactivation_service": "Command to turn-off"
|
||||
},
|
||||
"data_description": {
|
||||
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Self-regulation with valve",
|
||||
"description": "Configuration for self-regulation with direct control of the valve",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -243,6 +272,7 @@
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"valve_regulation": "Valve regulation configuration",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
@@ -279,7 +309,7 @@
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
@@ -418,6 +448,34 @@
|
||||
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Control of the central boiler - {name}",
|
||||
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Command to turn-on",
|
||||
"central_boiler_deactivation_service": "Command to turn-off"
|
||||
},
|
||||
"data_description": {
|
||||
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Self-regulation with valve - {name}",
|
||||
"description": "Configuration for self-regulation with direct control of the valve",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -425,7 +483,8 @@
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
|
||||
"service_configuration_format": "The format of the service configuration is wrong"
|
||||
"service_configuration_format": "The format of the service configuration is wrong",
|
||||
"valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -447,7 +506,8 @@
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "No auto-regulation"
|
||||
"auto_regulation_none": "No auto-regulation",
|
||||
"auto_regulation_valve": "Direct control of valve"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# pylint: disable=line-too-long, too-many-lines
|
||||
""" A climate over switch classe """
|
||||
# pylint: disable=line-too-long, too-many-lines, abstract-method
|
||||
""" A climate over climate classe """
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
)
|
||||
|
||||
from .commons import NowClass, round_to_nearest
|
||||
from .commons import round_to_nearest
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .pi_algorithm import PITemperatureRegulator
|
||||
|
||||
@@ -31,10 +31,6 @@ 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,
|
||||
@@ -46,46 +42,27 @@ HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||
class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
"""Representation of a base class for a Versatile Thermostat over a climate"""
|
||||
|
||||
_auto_regulation_mode: str | None = None
|
||||
_regulation_algo = None
|
||||
_regulated_target_temp: float | None = None
|
||||
_auto_regulation_dtemp: float | None = None
|
||||
_auto_regulation_period_min: int | None = None
|
||||
_last_regulation_change: datetime | None = None
|
||||
# The fan mode configured in configEntry
|
||||
_auto_fan_mode: str | None = None
|
||||
# The current fan mode (could be change by service call)
|
||||
_current_auto_fan_mode: str | None = None
|
||||
# The fan_mode name depending of the current_mode
|
||||
_auto_activated_fan_mode: str | None = None
|
||||
_auto_deactivated_fan_mode: str | None = None
|
||||
_auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = AUTO_START_STOP_LEVEL_NONE
|
||||
_auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
|
||||
_is_auto_start_stop_enabled: bool = False
|
||||
_follow_underlying_temp_change: bool = False
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
{
|
||||
"is_over_climate",
|
||||
"start_hvac_action_date",
|
||||
"underlying_entities",
|
||||
"regulation_accumulated_error",
|
||||
"auto_regulation_mode",
|
||||
"auto_fan_mode",
|
||||
"current_auto_fan_mode",
|
||||
"auto_activated_fan_mode",
|
||||
"auto_deactivated_fan_mode",
|
||||
"auto_regulation_use_device_temp",
|
||||
"auto_start_stop_level",
|
||||
"auto_start_stop_dtmin",
|
||||
"auto_start_stop_enable",
|
||||
"auto_start_stop_accumulated_error",
|
||||
"auto_start_stop_accumulated_error_threshold",
|
||||
"follow_underlying_temp_change",
|
||||
}
|
||||
)
|
||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
|
||||
frozenset(
|
||||
{
|
||||
"is_over_climate",
|
||||
"start_hvac_action_date",
|
||||
"underlying_entities",
|
||||
"regulation_accumulated_error",
|
||||
"auto_regulation_mode",
|
||||
"auto_fan_mode",
|
||||
"current_auto_fan_mode",
|
||||
"auto_activated_fan_mode",
|
||||
"auto_deactivated_fan_mode",
|
||||
"auto_regulation_use_device_temp",
|
||||
"auto_start_stop_level",
|
||||
"auto_start_stop_dtmin",
|
||||
"auto_start_stop_enable",
|
||||
"auto_start_stop_accumulated_error",
|
||||
"auto_start_stop_accumulated_error_threshold",
|
||||
"auto_start_stop_last_switch_date",
|
||||
"follow_underlying_temp_change",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -93,10 +70,30 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
|
||||
):
|
||||
"""Initialize the thermostat over switch."""
|
||||
self._auto_regulation_mode: str | None = None
|
||||
self._regulation_algo = None
|
||||
self._regulated_target_temp: float | None = None
|
||||
self._auto_regulation_dtemp: float | None = None
|
||||
self._auto_regulation_period_min: int | None = None
|
||||
self._last_regulation_change: datetime | None = None
|
||||
# The fan mode configured in configEntry
|
||||
self._auto_fan_mode: str | None = None
|
||||
# The current fan mode (could be change by service call)
|
||||
self._current_auto_fan_mode: str | None = None
|
||||
# The fan_mode name depending of the current_mode
|
||||
self._auto_activated_fan_mode: str | None = None
|
||||
self._auto_deactivated_fan_mode: str | None = None
|
||||
self._auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = (
|
||||
AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
self._auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
|
||||
self._is_auto_start_stop_enabled: bool = False
|
||||
self._follow_underlying_temp_change: bool = False
|
||||
self._last_regulation_change = None # NowClass.get_now(hass)
|
||||
|
||||
# super.__init__ calls post_init at the end. So it must be called after regulation initialization
|
||||
super().__init__(hass, unique_id, name, entry_infos)
|
||||
self._regulated_target_temp = self.target_temperature
|
||||
self._last_regulation_change = NowClass.get_now(hass)
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
@@ -105,13 +102,12 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
super().post_init(config_entry)
|
||||
|
||||
for climate in config_entry.get(CONF_UNDERLYING_LIST):
|
||||
self._underlyings.append(
|
||||
UnderlyingClimate(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
climate_entity_id=climate,
|
||||
)
|
||||
under = UnderlyingClimate(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
climate_entity_id=climate,
|
||||
)
|
||||
self._underlyings.append(under)
|
||||
|
||||
self.choose_auto_regulation_mode(
|
||||
config_entry.get(CONF_AUTO_REGULATION_MODE)
|
||||
@@ -158,15 +154,13 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
"""True if the Thermostat is over_climate"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Returns the current hvac_action by checking all hvac_action of the underlyings"""
|
||||
|
||||
def calculate_hvac_action(self, under_list: list) -> HVACAction | None:
|
||||
"""Calculate an hvac action based on the hvac_action of the list in argument"""
|
||||
# if one not IDLE or OFF -> return it
|
||||
# else if one IDLE -> IDLE
|
||||
# else OFF
|
||||
one_idle = False
|
||||
for under in self._underlyings:
|
||||
for under in under_list:
|
||||
if (action := under.hvac_action) not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
@@ -178,13 +172,19 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
return HVACAction.IDLE
|
||||
return HVACAction.OFF
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Returns the current hvac_action by checking all hvac_action of the underlyings"""
|
||||
return self.calculate_hvac_action(self._underlyings)
|
||||
|
||||
@overrides
|
||||
async def _async_internal_set_temperature(self, temperature: float):
|
||||
"""Set the target temperature and the target temperature of underlying climate if any"""
|
||||
await super()._async_internal_set_temperature(temperature)
|
||||
|
||||
self._regulation_algo.set_target_temp(self.target_temperature)
|
||||
await self._send_regulated_temperature(force=True)
|
||||
# is done by control_heating method. No need to do it here
|
||||
# await self._send_regulated_temperature(force=True)
|
||||
|
||||
async def _send_regulated_temperature(self, force=False):
|
||||
"""Sends the regulated temperature to all underlying"""
|
||||
@@ -209,16 +209,18 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
force,
|
||||
)
|
||||
|
||||
now: datetime = NowClass.get_now(self._hass)
|
||||
period = float((now - self._last_regulation_change).total_seconds()) / 60.0
|
||||
if not force and period < self._auto_regulation_period_min:
|
||||
_LOGGER.info(
|
||||
"%s - period (%.1f) min is < %.0f min -> forget the regulation send",
|
||||
self,
|
||||
period,
|
||||
self._auto_regulation_period_min,
|
||||
if self._last_regulation_change is not None:
|
||||
period = (
|
||||
float((self.now - self._last_regulation_change).total_seconds()) / 60.0
|
||||
)
|
||||
return
|
||||
if not force and period < self._auto_regulation_period_min:
|
||||
_LOGGER.info(
|
||||
"%s - period (%.1f) min is < %.0f min -> forget the regulation send",
|
||||
self,
|
||||
period,
|
||||
self._auto_regulation_period_min,
|
||||
)
|
||||
return
|
||||
|
||||
if not self._regulated_target_temp:
|
||||
self._regulated_target_temp = self.target_temperature
|
||||
@@ -256,7 +258,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
new_regulated_temp,
|
||||
)
|
||||
|
||||
self._last_regulation_change = now
|
||||
self._last_regulation_change = self.now
|
||||
for under in self._underlyings:
|
||||
# issue 348 - use device temperature if configured as offset
|
||||
offset_temp = 0
|
||||
@@ -554,6 +556,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
"auto_start_stop_accumulated_error_threshold"
|
||||
] = self._auto_start_stop_algo.accumulated_error_threshold
|
||||
|
||||
self._attr_extra_state_attributes["auto_start_stop_last_switch_date"] = (
|
||||
self._auto_start_stop_algo.last_switch_date
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["follow_underlying_temp_change"] = (
|
||||
self._follow_underlying_temp_change
|
||||
)
|
||||
@@ -606,14 +612,14 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
_LOGGER.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
_LOGGER.debug(
|
||||
"%s - incremente_energy incremented energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
@@ -928,7 +934,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
|
||||
# Stop here
|
||||
return False
|
||||
elif action == AUTO_START_STOP_ACTION_ON:
|
||||
elif (
|
||||
action == AUTO_START_STOP_ACTION_ON
|
||||
and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self
|
||||
)
|
||||
@@ -966,7 +975,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
if not continu:
|
||||
return ret
|
||||
else:
|
||||
_LOGGER.debug("%s - auto start/stop is disabled")
|
||||
_LOGGER.debug("%s - auto start/stop is disabled", self)
|
||||
|
||||
# Continue the normal async_control_heating
|
||||
|
||||
@@ -981,6 +990,32 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
def set_auto_start_stop_enable(self, is_enabled: bool):
|
||||
"""Enable/Disable the auto-start/stop feature"""
|
||||
self._is_auto_start_stop_enabled = is_enabled
|
||||
if (
|
||||
self.hvac_mode == HVACMode.OFF
|
||||
and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - the vtherm is off cause auto-start/stop and enable have been set to false -> starts the VTherm"
|
||||
)
|
||||
self.hass.create_task(self.async_turn_on())
|
||||
|
||||
# Send an event
|
||||
self.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "start",
|
||||
"name": self.name,
|
||||
"cause": "Auto start stop disabled",
|
||||
"hvac_mode": self.hvac_mode,
|
||||
"saved_hvac_mode": self._saved_hvac_mode,
|
||||
"target_temperature": self.target_temperature,
|
||||
"current_temperature": self.current_temperature,
|
||||
"temperature_slope": round(self.last_temperature_slope or 0, 3),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
def set_follow_underlying_temp_change(self, follow: bool):
|
||||
@@ -1084,15 +1119,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
|
||||
return self._support_flags
|
||||
|
||||
# We keep the step configured for the VTherm and not the step of the underlying
|
||||
# @property
|
||||
# def target_temperature_step(self) -> float | None:
|
||||
# """Return the supported step of target temperature."""
|
||||
# if self.underlying_entity(0):
|
||||
# return self.underlying_entity(0).target_temperature_step
|
||||
#
|
||||
# return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
"""Return the highbound target temperature we try to reach.
|
||||
@@ -1115,6 +1141,14 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_humidity(self) -> float | None:
|
||||
"""Return the humidity."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).current_humidity
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool | None:
|
||||
"""Return true if aux heater.
|
||||
@@ -1251,6 +1285,13 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_SLOW)
|
||||
elif auto_regulation_mode == "Expert":
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_EXPERT)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"%s - auto_regulation_mode %s is not supported",
|
||||
self,
|
||||
auto_regulation_mode,
|
||||
)
|
||||
return
|
||||
|
||||
await self._send_regulated_temperature()
|
||||
self.update_custom_attributes()
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
# pylint: disable=line-too-long, too-many-lines, abstract-method
|
||||
""" A climate with a direct valve regulation class """
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import HVACMode, HVACAction
|
||||
|
||||
from .underlyings import UnderlyingValveRegulation
|
||||
|
||||
# from .commons import NowClass, round_to_nearest
|
||||
from .base_thermostat import ConfigData
|
||||
from .thermostat_climate import ThermostatOverClimate
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
# from .vtherm_api import VersatileThermostatAPI
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
"""This class represent a VTherm over a climate with a direct valve regulation"""
|
||||
|
||||
_entity_component_unrecorded_attributes = ThermostatOverClimate._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
|
||||
frozenset(
|
||||
{
|
||||
"is_over_climate",
|
||||
"have_valve_regulation",
|
||||
"underlying_entities",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"power_percent",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
|
||||
):
|
||||
"""Initialize the ThermostatOverClimateValve class"""
|
||||
_LOGGER.debug("%s - creating a ThermostatOverClimateValve VTherm", name)
|
||||
self._underlyings_valve_regulation: list[UnderlyingValveRegulation] = []
|
||||
self._valve_open_percent: int | None = None
|
||||
self._last_calculation_timestamp: datetime | None = None
|
||||
self._auto_regulation_dpercent: float | None = None
|
||||
self._auto_regulation_period_min: int | None = None
|
||||
|
||||
super().__init__(hass, unique_id, name, entry_infos)
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat and underlyings
|
||||
Beware that the underlyings list contains the climate which represent the TRV
|
||||
but also the UnderlyingValveRegulation which reprensent the valve"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
|
||||
self._auto_regulation_dpercent = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||
else 0.0
|
||||
)
|
||||
self._auto_regulation_period_min = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
|
||||
else 0
|
||||
)
|
||||
|
||||
# Initialization of the TPI algo
|
||||
self._prop_algorithm = PropAlgorithm(
|
||||
self._proportional_function,
|
||||
self._tpi_coef_int,
|
||||
self._tpi_coef_ext,
|
||||
self._cycle_min,
|
||||
self._minimal_activation_delay,
|
||||
self.name,
|
||||
)
|
||||
|
||||
offset_list = config_entry.get(CONF_OFFSET_CALIBRATION_LIST, [])
|
||||
opening_list = config_entry.get(CONF_OPENING_DEGREE_LIST)
|
||||
closing_list = config_entry.get(CONF_CLOSING_DEGREE_LIST, [])
|
||||
for idx, _ in enumerate(config_entry.get(CONF_UNDERLYING_LIST)):
|
||||
offset = offset_list[idx] if idx < len(offset_list) else None
|
||||
# number of opening should equal number of underlying
|
||||
opening = opening_list[idx]
|
||||
closing = closing_list[idx] if idx < len(closing_list) else None
|
||||
under = UnderlyingValveRegulation(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
offset_calibration_entity_id=offset,
|
||||
opening_degree_entity_id=opening,
|
||||
closing_degree_entity_id=closing,
|
||||
climate_underlying=self._underlyings[idx],
|
||||
)
|
||||
self._underlyings_valve_regulation.append(under)
|
||||
|
||||
@overrides
|
||||
def update_custom_attributes(self):
|
||||
"""Custom attributes"""
|
||||
super().update_custom_attributes()
|
||||
|
||||
self._attr_extra_state_attributes["have_valve_regulation"] = (
|
||||
self.have_valve_regulation
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["underlyings_valve_regulation"] = [
|
||||
underlying.valve_entity_ids
|
||||
for underlying in self._underlyings_valve_regulation
|
||||
]
|
||||
|
||||
self._attr_extra_state_attributes["on_percent"] = (
|
||||
self._prop_algorithm.on_percent
|
||||
)
|
||||
self._attr_extra_state_attributes["power_percent"] = self.power_percent
|
||||
self._attr_extra_state_attributes["on_time_sec"] = (
|
||||
self._prop_algorithm.on_time_sec
|
||||
)
|
||||
self._attr_extra_state_attributes["off_time_sec"] = (
|
||||
self._prop_algorithm.off_time_sec
|
||||
)
|
||||
self._attr_extra_state_attributes["cycle_min"] = self._cycle_min
|
||||
self._attr_extra_state_attributes["function"] = self._proportional_function
|
||||
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
|
||||
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
|
||||
|
||||
self._attr_extra_state_attributes["valve_open_percent"] = (
|
||||
self.valve_open_percent
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["auto_regulation_dpercent"] = (
|
||||
self._auto_regulation_dpercent
|
||||
)
|
||||
self._attr_extra_state_attributes["auto_regulation_period_min"] = (
|
||||
self._auto_regulation_period_min
|
||||
)
|
||||
self._attr_extra_state_attributes["last_calculation_timestamp"] = (
|
||||
self._last_calculation_timestamp.astimezone(self._current_tz).isoformat()
|
||||
if self._last_calculation_timestamp
|
||||
else None
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
"%s - Calling update_custom_attributes: %s",
|
||||
self,
|
||||
self._attr_extra_state_attributes,
|
||||
)
|
||||
|
||||
@overrides
|
||||
def recalculate(self):
|
||||
"""A utility function to force the calculation of a the algo and
|
||||
update the custom attributes and write the state
|
||||
"""
|
||||
_LOGGER.debug("%s - recalculate the open percent", self)
|
||||
|
||||
# TODO this is exactly the same method as the thermostat_valve recalculate. Put that in common
|
||||
|
||||
# For testing purpose. Should call _set_now() before
|
||||
now = self.now
|
||||
|
||||
if self._last_calculation_timestamp is not None:
|
||||
period = (now - self._last_calculation_timestamp).total_seconds() / 60
|
||||
if period < self._auto_regulation_period_min:
|
||||
_LOGGER.info(
|
||||
"%s - do not calculate TPI because regulation_period (%d) is not exceeded",
|
||||
self,
|
||||
period,
|
||||
)
|
||||
return
|
||||
|
||||
self._prop_algorithm.calculate(
|
||||
self._target_temp,
|
||||
self._cur_temp,
|
||||
self._cur_ext_temp,
|
||||
self._hvac_mode or HVACMode.OFF,
|
||||
)
|
||||
|
||||
new_valve_percent = round(
|
||||
max(0, min(self.proportional_algorithm.on_percent, 1)) * 100
|
||||
)
|
||||
|
||||
# Issue 533 - don't filter with dtemp if valve should be close. Else it will never close
|
||||
if new_valve_percent < self._auto_regulation_dpercent:
|
||||
new_valve_percent = 0
|
||||
|
||||
dpercent = (
|
||||
new_valve_percent - self._valve_open_percent
|
||||
if self._valve_open_percent is not None
|
||||
else 0
|
||||
)
|
||||
if (
|
||||
self._last_calculation_timestamp is not None
|
||||
and new_valve_percent > 0
|
||||
and -1 * self._auto_regulation_dpercent
|
||||
<= dpercent
|
||||
< self._auto_regulation_dpercent
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - do not calculate TPI because regulation_dpercent (%.1f) is not exceeded",
|
||||
self,
|
||||
dpercent,
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
if (
|
||||
self._last_calculation_timestamp is not None
|
||||
and self._valve_open_percent == new_valve_percent
|
||||
):
|
||||
_LOGGER.debug("%s - no change in valve_open_percent.", self)
|
||||
return
|
||||
|
||||
self._valve_open_percent = new_valve_percent
|
||||
|
||||
self._last_calculation_timestamp = now
|
||||
|
||||
super().recalculate()
|
||||
|
||||
async def _send_regulated_temperature(self, force=False):
|
||||
"""Sends the regulated temperature to all underlying"""
|
||||
if self.target_temperature is None:
|
||||
return
|
||||
|
||||
for under in self._underlyings:
|
||||
if self.target_temperature != under.last_sent_temperature:
|
||||
await under.set_temperature(
|
||||
self.target_temperature,
|
||||
self._attr_max_temp,
|
||||
self._attr_min_temp,
|
||||
)
|
||||
|
||||
for under in self._underlyings_valve_regulation:
|
||||
await under.set_valve_open_percent()
|
||||
|
||||
@property
|
||||
def have_valve_regulation(self) -> bool:
|
||||
"""True if the Thermostat is regulated by valve"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def power_percent(self) -> float | None:
|
||||
"""Get the current on_percent value"""
|
||||
if self._prop_algorithm:
|
||||
return round(self._prop_algorithm.on_percent * 100, 0)
|
||||
else:
|
||||
return None
|
||||
|
||||
# @property
|
||||
# def hvac_modes(self) -> list[HVACMode]:
|
||||
# """Get the hvac_modes"""
|
||||
# return self._hvac_list
|
||||
|
||||
@property
|
||||
def valve_open_percent(self) -> int:
|
||||
"""Gives the percentage of valve needed"""
|
||||
if self._hvac_mode == HVACMode.OFF or self._valve_open_percent is None:
|
||||
return 0
|
||||
else:
|
||||
return self._valve_open_percent
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Returns the current hvac_action by checking all hvac_action of the _underlyings_valve_regulation"""
|
||||
|
||||
return self.calculate_hvac_action(self._underlyings_valve_regulation)
|
||||
|
||||
@property
|
||||
def is_device_active(self) -> bool:
|
||||
"""A hack to overrides the state from underlyings"""
|
||||
return self.valve_open_percent > 0
|
||||
|
||||
@property
|
||||
def nb_device_actives(self) -> int:
|
||||
"""Calculate the number of active devices"""
|
||||
if self.is_device_active:
|
||||
return len(self._underlyings_valve_regulation)
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def activable_underlying_entities(self) -> list | None:
|
||||
"""Returns the activable underlying entities for controling the central boiler"""
|
||||
return self._underlyings_valve_regulation
|
||||
|
||||
@overrides
|
||||
async def service_set_auto_regulation_mode(self, auto_regulation_mode: str):
|
||||
"""This should not be possible in valve regulation mode"""
|
||||
return
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=line-too-long
|
||||
# pylint: disable=line-too-long, abstract-method
|
||||
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
@@ -7,6 +7,7 @@ from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .const import (
|
||||
@@ -21,9 +22,6 @@ 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."""
|
||||
@@ -48,11 +46,10 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
)
|
||||
)
|
||||
|
||||
# useless for now
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||
# """Initialize the thermostat over switch."""
|
||||
# super().__init__(hass, unique_id, name, config_entry)
|
||||
_is_inversed: bool | None = None
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||
"""Initialize the thermostat over switch."""
|
||||
self._is_inversed: bool | None = None
|
||||
super().__init__(hass, unique_id, name, config_entry)
|
||||
|
||||
@property
|
||||
def is_over_switch(self) -> bool:
|
||||
@@ -190,14 +187,14 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
_LOGGER.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
_LOGGER.debug(
|
||||
"%s - incremente_energy increment energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=line-too-long
|
||||
# pylint: disable=line-too-long, abstract-method
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
@@ -25,9 +25,6 @@ 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"""
|
||||
@@ -251,8 +248,9 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
|
||||
self._valve_open_percent = new_valve_percent
|
||||
|
||||
for under in self._underlyings:
|
||||
under.set_valve_open_percent()
|
||||
# is one in start_cycle now
|
||||
# for under in self._underlyings:
|
||||
# under.set_valve_open_percent()
|
||||
|
||||
self._last_calculation_timestamp = now
|
||||
|
||||
@@ -272,14 +270,14 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
_LOGGER.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER_ENERGY.debug(
|
||||
_LOGGER.debug(
|
||||
"%s - get_my_previous_state increment energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"valve_regulation": "Valve regulation configuration",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
@@ -203,6 +204,34 @@
|
||||
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Control of the central boiler",
|
||||
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Command to turn-on",
|
||||
"central_boiler_deactivation_service": "Command to turn-off"
|
||||
},
|
||||
"data_description": {
|
||||
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Self-regulation with valve",
|
||||
"description": "Configuration for self-regulation with direct control of the valve",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -243,6 +272,7 @@
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"valve_regulation": "Valve regulation configuration",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
@@ -279,7 +309,7 @@
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
@@ -418,6 +448,34 @@
|
||||
"security_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset",
|
||||
"use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm"
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Control of the central boiler - {name}",
|
||||
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Command to turn-on",
|
||||
"central_boiler_deactivation_service": "Command to turn-off"
|
||||
},
|
||||
"data_description": {
|
||||
"central_boiler_activation_service": "Command to turn-on the central boiler formatted like entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Self-regulation with valve - {name}",
|
||||
"description": "Configuration for self-regulation with direct control of the valve",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -425,7 +483,8 @@
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
|
||||
"service_configuration_format": "The format of the service configuration is wrong"
|
||||
"service_configuration_format": "The format of the service configuration is wrong",
|
||||
"valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -447,7 +506,8 @@
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "No auto-regulation"
|
||||
"auto_regulation_none": "No auto-regulation",
|
||||
"auto_regulation_valve": "Direct control of valve"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
@@ -547,4 +607,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
"presence": "Détection de présence",
|
||||
"advanced": "Paramètres avancés",
|
||||
"auto_start_stop": "Allumage/extinction automatique",
|
||||
"valve_regulation": "Configuration de la regulation par vanne",
|
||||
"finalize": "Finaliser la création",
|
||||
"configuration_not_complete": "Configuration incomplète"
|
||||
}
|
||||
@@ -72,48 +73,26 @@
|
||||
"title": "Entité(s) liée(s)",
|
||||
"description": "Attributs de(s) l'entité(s) liée(s)",
|
||||
"data": {
|
||||
"heater_entity_id": "1er radiateur",
|
||||
"heater_entity2_id": "2ème radiateur",
|
||||
"heater_entity3_id": "3ème radiateur",
|
||||
"heater_entity4_id": "4ème radiateur",
|
||||
"underlying_entity_ids": "Les équipements à controller",
|
||||
"heater_keep_alive": "keep-alive (sec)",
|
||||
"proportional_function": "Algorithme",
|
||||
"climate_entity_id": "Thermostat sous-jacent",
|
||||
"climate_entity2_id": "2ème thermostat sous-jacent",
|
||||
"climate_entity3_id": "3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "4ème thermostat sous-jacent",
|
||||
"ac_mode": "AC mode ?",
|
||||
"valve_entity_id": "1ère valve number",
|
||||
"valve_entity2_id": "2ème valve number",
|
||||
"valve_entity3_id": "3ème valve number",
|
||||
"valve_entity4_id": "4ème valve number",
|
||||
"auto_regulation_mode": "Auto-régulation",
|
||||
"auto_regulation_dtemp": "Seuil de régulation",
|
||||
"auto_regulation_periode_min": "Période minimale de régulation",
|
||||
"auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent",
|
||||
"auto_regulation_use_device_temp": "Compenser la température interne du sous-jacent",
|
||||
"inverse_switch_command": "Inverser la commande",
|
||||
"auto_fan_mode": " Auto ventilation mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
|
||||
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
|
||||
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
|
||||
"underlying_entity_ids": "La liste des équipements qui seront controlés par ce VTherm",
|
||||
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
|
||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||
"climate_entity_id": "Entity id du thermostat sous-jacent",
|
||||
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
|
||||
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
|
||||
"ac_mode": "Utilisation du mode Air Conditionné (AC)",
|
||||
"valve_entity_id": "Entity id de la 1ère valve",
|
||||
"valve_entity2_id": "Entity id de la 2ème valve",
|
||||
"valve_entity3_id": "Entity id de la 3ème valve",
|
||||
"valve_entity4_id": "Entity id de la 4ème valve",
|
||||
"auto_regulation_mode": "Ajustement automatique de la température cible",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_mode": "Utilisation de l'auto-régulation faite par VTherm",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les vannes) en-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
|
||||
"auto_regulation_use_device_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
|
||||
"auto_regulation_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
|
||||
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
|
||||
}
|
||||
@@ -237,6 +216,22 @@
|
||||
"central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Auto-régulation par vanne - {name}",
|
||||
"description": "Configuration de l'auto-régulation par controle direct de la vanne",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
|
||||
"opening_degree_entity_ids": "Entités 'ouverture de vanne'",
|
||||
"closing_degree_entity_ids": "Entités 'fermeture de la vanne'",
|
||||
"proportional_function": "Algorithme"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -262,7 +257,7 @@
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu",
|
||||
"title": "Menu - {name}",
|
||||
"description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.",
|
||||
"menu_options": {
|
||||
"main": "Principaux Attributs",
|
||||
@@ -277,6 +272,7 @@
|
||||
"presence": "Détection de présence",
|
||||
"advanced": "Paramètres avancés",
|
||||
"auto_start_stop": "Allumage/extinction automatique",
|
||||
"valve_regulation": "Configuration de la regulation par vanne",
|
||||
"finalize": "Finaliser les modifications",
|
||||
"configuration_not_complete": "Configuration incomplète"
|
||||
}
|
||||
@@ -318,51 +314,29 @@
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entités - {name}",
|
||||
"title": "Entité(s) liée(s) - {name}",
|
||||
"description": "Attributs de(s) l'entité(s) liée(s)",
|
||||
"data": {
|
||||
"heater_entity_id": "1er radiateur",
|
||||
"heater_entity2_id": "2ème radiateur",
|
||||
"heater_entity3_id": "3ème radiateur",
|
||||
"heater_entity4_id": "4ème radiateur",
|
||||
"heater_keep_alive": "Keep-alive (sec)",
|
||||
"underlying_entity_ids": "Les équipements à controller",
|
||||
"heater_keep_alive": "keep-alive (sec)",
|
||||
"proportional_function": "Algorithme",
|
||||
"climate_entity_id": "Thermostat sous-jacent",
|
||||
"climate_entity2_id": "2ème thermostat sous-jacent",
|
||||
"climate_entity3_id": "3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "4ème thermostat sous-jacent",
|
||||
"ac_mode": "AC mode ?",
|
||||
"valve_entity_id": "1ère valve",
|
||||
"valve_entity2_id": "2ème valve",
|
||||
"valve_entity3_id": "3ème valve",
|
||||
"valve_entity4_id": "4ème valve",
|
||||
"auto_regulation_mode": "Auto-regulation",
|
||||
"auto_regulation_mode": "Auto-régulation",
|
||||
"auto_regulation_dtemp": "Seuil de régulation",
|
||||
"auto_regulation_periode_min": "Période minimale de régulation",
|
||||
"auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent",
|
||||
"auto_regulation_use_device_temp": "Compenser la température interne du sous-jacent",
|
||||
"inverse_switch_command": "Inverser la commande",
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
"auto_fan_mode": " Auto ventilation mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
|
||||
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
|
||||
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
|
||||
"underlying_entity_ids": "La liste des équipements qui seront controlés par ce VTherm",
|
||||
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
|
||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||
"climate_entity_id": "Entity id du thermostat sous-jacent",
|
||||
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
|
||||
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
|
||||
"ac_mode": "Utilisation du mode Air Conditionné (AC)",
|
||||
"valve_entity_id": "Entity id de la 1ère valve",
|
||||
"valve_entity2_id": "Entity id de la 2ème valve",
|
||||
"valve_entity3_id": "Entity id de la 3ème valve",
|
||||
"valve_entity4_id": "Entity id de la 4ème valve",
|
||||
"auto_regulation_mode": "Ajustement automatique de la consigne",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_mode": "Utilisation de l'auto-régulation faite par VTherm",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les vannes) en-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
|
||||
"auto_regulation_use_device_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
|
||||
"auto_regulation_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
|
||||
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
|
||||
}
|
||||
@@ -480,6 +454,22 @@
|
||||
"central_boiler_activation_service": "Commande à éxecuter pour allumer la chaudière centrale au format entity_id/service_name[/attribut:valeur]",
|
||||
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Auto-régulation par vanne - {name}",
|
||||
"description": "Configuration de l'auto-régulation par controle direct de la vanne",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
|
||||
"opening_degree_entity_ids": "Entités 'ouverture de vanne'",
|
||||
"closing_degree_entity_ids": "Entités 'fermeture de la vanne'",
|
||||
"proportional_function": "Algorithme"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -487,7 +477,8 @@
|
||||
"unknown_entity": "entity id inconnu",
|
||||
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
|
||||
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.",
|
||||
"service_configuration_format": "Mauvais format de la configuration du service"
|
||||
"service_configuration_format": "Mauvais format de la configuration du service",
|
||||
"valve_regulation_nb_entities_incorrect": "Le nombre d'entités pour la régulation par vanne doit être égal au nombre d'entité sous-jacentes"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
@@ -509,7 +500,8 @@
|
||||
"auto_regulation_medium": "Moyenne",
|
||||
"auto_regulation_light": "Légère",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "Aucune"
|
||||
"auto_regulation_none": "Aucune",
|
||||
"auto_regulation_valve": "Contrôle direct de la vanne"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=unused-argument, line-too-long
|
||||
# pylint: disable=unused-argument, line-too-long, too-many-lines
|
||||
|
||||
""" Underlying entities classes """
|
||||
import logging
|
||||
@@ -32,7 +32,7 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .const import UnknownEntity, overrides
|
||||
from .const import UnknownEntity, overrides, get_safe_float
|
||||
from .keep_alive import IntervalCaller
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -53,6 +53,9 @@ class UnderlyingEntityType(StrEnum):
|
||||
# a valve
|
||||
VALVE = "valve"
|
||||
|
||||
# a direct valve regulation
|
||||
VALVE_REGULATION = "valve_regulation"
|
||||
|
||||
|
||||
class UnderlyingEntity:
|
||||
"""Represent a underlying device which could be a switch or a climate"""
|
||||
@@ -62,6 +65,7 @@ class UnderlyingEntity:
|
||||
_thermostat: Any
|
||||
_entity_id: str
|
||||
_type: UnderlyingEntityType
|
||||
_hvac_mode: HVACMode | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -75,6 +79,7 @@ class UnderlyingEntity:
|
||||
self._thermostat = thermostat
|
||||
self._type = entity_type
|
||||
self._entity_id = entity_id
|
||||
self._hvac_mode = None
|
||||
|
||||
def __str__(self):
|
||||
return str(self._thermostat) + "-" + self._entity_id
|
||||
@@ -100,13 +105,24 @@ class UnderlyingEntity:
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode):
|
||||
"""Set the HVACmode"""
|
||||
self._hvac_mode = hvac_mode
|
||||
return
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
"""Return the current hvac_mode"""
|
||||
return self._hvac_mode
|
||||
|
||||
@property
|
||||
def is_device_active(self) -> bool | None:
|
||||
"""If the toggleable device is currently active."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction:
|
||||
"""Calculate a hvac_action"""
|
||||
return HVACAction.HEATING if self.is_device_active is True else HVACAction.OFF
|
||||
|
||||
async def set_temperature(self, temperature, max_temp, min_temp):
|
||||
"""Set the target temperature"""
|
||||
return
|
||||
@@ -181,7 +197,6 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
_initialDelaySec: int
|
||||
_on_time_sec: int
|
||||
_off_time_sec: int
|
||||
_hvac_mode: HVACMode
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -204,7 +219,6 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
self._should_relaunch_control_heating = False
|
||||
self._on_time_sec = 0
|
||||
self._off_time_sec = 0
|
||||
self._hvac_mode = None
|
||||
self._keep_alive = IntervalCaller(hass, keep_alive_sec)
|
||||
|
||||
@property
|
||||
@@ -237,8 +251,8 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
await self.turn_off()
|
||||
self._cancel_cycle()
|
||||
|
||||
if self._hvac_mode != hvac_mode:
|
||||
self._hvac_mode = hvac_mode
|
||||
if self.hvac_mode != hvac_mode:
|
||||
super().set_hvac_mode(hvac_mode)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -713,6 +727,13 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
return []
|
||||
return self._underlying_climate.hvac_modes
|
||||
|
||||
@property
|
||||
def current_humidity(self) -> float | None:
|
||||
"""Get the humidity"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
return self._underlying_climate.current_humidity
|
||||
|
||||
@property
|
||||
def fan_modes(self) -> list[str]:
|
||||
"""Get the fan_modes"""
|
||||
@@ -847,11 +868,16 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
_hvac_mode: HVACMode
|
||||
# This is the percentage of opening int integer (from 0 to 100)
|
||||
_percent_open: int
|
||||
_last_sent_temperature = None
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
thermostat: Any,
|
||||
valve_entity_id: str,
|
||||
entity_type: UnderlyingEntityType = UnderlyingEntityType.VALVE,
|
||||
) -> None:
|
||||
"""Initialize the underlying switch"""
|
||||
"""Initialize the underlying valve"""
|
||||
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
@@ -862,16 +888,15 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
self._async_cancel_cycle = None
|
||||
self._should_relaunch_control_heating = False
|
||||
self._hvac_mode = None
|
||||
self._percent_open = self._thermostat.valve_open_percent
|
||||
self._percent_open = None # self._thermostat.valve_open_percent
|
||||
self._valve_entity_id = valve_entity_id
|
||||
|
||||
async def send_percent_open(self):
|
||||
"""Send the percent open to the underlying valve"""
|
||||
# This may fails if called after shutdown
|
||||
async def _send_value_to_number(self, number_entity_id: str, value: int):
|
||||
"""Send a value to a number entity"""
|
||||
try:
|
||||
data = {"value": self._percent_open}
|
||||
target = {ATTR_ENTITY_ID: self._entity_id}
|
||||
domain = self._entity_id.split(".")[0]
|
||||
data = {"value": value}
|
||||
target = {ATTR_ENTITY_ID: number_entity_id}
|
||||
domain = number_entity_id.split(".")[0]
|
||||
await self._hass.services.async_call(
|
||||
domain=domain,
|
||||
service=SERVICE_SET_VALUE,
|
||||
@@ -883,6 +908,11 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
# This could happens in unit test if input_number domain is not yet loaded
|
||||
# raise err
|
||||
|
||||
async def send_percent_open(self):
|
||||
"""Send the percent open to the underlying valve"""
|
||||
# This may fails if called after shutdown
|
||||
return await self._send_value_to_number(self._entity_id, self._percent_open)
|
||||
|
||||
async def turn_off(self):
|
||||
"""Turn heater toggleable device off."""
|
||||
_LOGGER.debug("%s - Stopping underlying valve entity %s", self, self._entity_id)
|
||||
@@ -894,7 +924,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
|
||||
async def turn_on(self):
|
||||
"""Nothing to do for Valve because it cannot be turned on"""
|
||||
self.set_valve_open_percent()
|
||||
await self.set_valve_open_percent()
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
|
||||
"""Set the HVACmode. Returns true if something have change"""
|
||||
@@ -932,11 +962,8 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
force=False,
|
||||
):
|
||||
"""We use this function to change the on_percent"""
|
||||
if force:
|
||||
# self._percent_open = self.cap_sent_value(self._percent_open)
|
||||
# await self.send_percent_open()
|
||||
# avoid to send 2 times the same value at startup
|
||||
self.set_valve_open_percent()
|
||||
# if force:
|
||||
await self.set_valve_open_percent()
|
||||
|
||||
@overrides
|
||||
def cap_sent_value(self, value) -> float:
|
||||
@@ -969,7 +996,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
|
||||
return new_value
|
||||
|
||||
def set_valve_open_percent(self):
|
||||
async def set_valve_open_percent(self):
|
||||
"""Update the valve open percent"""
|
||||
caped_val = self.cap_sent_value(self._thermostat.valve_open_percent)
|
||||
if self._percent_open == caped_val:
|
||||
@@ -983,8 +1010,181 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
"%s - Setting valve ouverture percent to %s", self, self._percent_open
|
||||
)
|
||||
# Send the change to the valve, in background
|
||||
self._hass.create_task(self.send_percent_open())
|
||||
# self._hass.create_task(self.send_percent_open())
|
||||
await self.send_percent_open()
|
||||
|
||||
def remove_entity(self):
|
||||
"""Remove the entity after stopping its cycle"""
|
||||
self._cancel_cycle()
|
||||
|
||||
|
||||
class UnderlyingValveRegulation(UnderlyingValve):
|
||||
"""A specific underlying class for Valve regulation"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
thermostat: Any,
|
||||
offset_calibration_entity_id: str,
|
||||
opening_degree_entity_id: str,
|
||||
closing_degree_entity_id: str,
|
||||
climate_underlying: UnderlyingClimate,
|
||||
) -> None:
|
||||
"""Initialize the underlying TRV with valve regulation"""
|
||||
super().__init__(
|
||||
hass,
|
||||
thermostat,
|
||||
opening_degree_entity_id,
|
||||
entity_type=UnderlyingEntityType.VALVE_REGULATION,
|
||||
)
|
||||
self._offset_calibration_entity_id: str = offset_calibration_entity_id
|
||||
self._opening_degree_entity_id: str = opening_degree_entity_id
|
||||
self._closing_degree_entity_id: str = closing_degree_entity_id
|
||||
self._climate_underlying = climate_underlying
|
||||
self._is_min_max_initialized: bool = False
|
||||
self._max_opening_degree: float = None
|
||||
self._min_offset_calibration: float = None
|
||||
self._max_offset_calibration: float = None
|
||||
|
||||
async def send_percent_open(self):
|
||||
"""Send the percent open to the underlying valve"""
|
||||
if not self._is_min_max_initialized:
|
||||
_LOGGER.debug(
|
||||
"%s - initialize min offset_calibration and max open_degree", self
|
||||
)
|
||||
self._max_opening_degree = self._hass.states.get(
|
||||
self._opening_degree_entity_id
|
||||
).attributes.get("max")
|
||||
|
||||
if self.have_offset_calibration_entity:
|
||||
self._min_offset_calibration = self._hass.states.get(
|
||||
self._offset_calibration_entity_id
|
||||
).attributes.get("min")
|
||||
self._max_offset_calibration = self._hass.states.get(
|
||||
self._offset_calibration_entity_id
|
||||
).attributes.get("max")
|
||||
|
||||
self._is_min_max_initialized = self._max_opening_degree is not None and (
|
||||
not self.have_offset_calibration_entity
|
||||
or (
|
||||
self._min_offset_calibration is not None
|
||||
and self._max_offset_calibration is not None
|
||||
)
|
||||
)
|
||||
|
||||
if not self._is_min_max_initialized:
|
||||
_LOGGER.warning(
|
||||
"%s - impossible to initialize max_opening_degree or min_offset_calibration. Abort sending percent open to the valve. This could be a temporary message at startup."
|
||||
)
|
||||
return
|
||||
|
||||
# Send opening_degree
|
||||
await super().send_percent_open()
|
||||
|
||||
# Send closing_degree if set
|
||||
closing_degree = None
|
||||
if self.have_closing_degree_entity:
|
||||
await self._send_value_to_number(
|
||||
self._closing_degree_entity_id,
|
||||
closing_degree := self._max_opening_degree - self._percent_open,
|
||||
)
|
||||
|
||||
# send offset_calibration to the difference between target temp and local temp
|
||||
offset = None
|
||||
if self.have_offset_calibration_entity:
|
||||
if (
|
||||
(local_temp := self._climate_underlying.underlying_current_temperature)
|
||||
is not None
|
||||
and (room_temp := self._thermostat.current_temperature) is not None
|
||||
and (
|
||||
current_offset := get_safe_float(
|
||||
self._hass, self._offset_calibration_entity_id
|
||||
)
|
||||
)
|
||||
is not None
|
||||
):
|
||||
offset = min(
|
||||
self._max_offset_calibration,
|
||||
max(
|
||||
self._min_offset_calibration,
|
||||
room_temp - (local_temp - current_offset),
|
||||
),
|
||||
)
|
||||
|
||||
await self._send_value_to_number(
|
||||
self._offset_calibration_entity_id, offset
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - valve regulation - I have sent offset_calibration=%s opening_degree=%s closing_degree=%s",
|
||||
self,
|
||||
offset,
|
||||
self._percent_open,
|
||||
closing_degree,
|
||||
)
|
||||
|
||||
@property
|
||||
def offset_calibration_entity_id(self) -> str:
|
||||
"""The offset_calibration_entity_id"""
|
||||
return self._offset_calibration_entity_id
|
||||
|
||||
@property
|
||||
def opening_degree_entity_id(self) -> str:
|
||||
"""The offset_calibration_entity_id"""
|
||||
return self._opening_degree_entity_id
|
||||
|
||||
@property
|
||||
def closing_degree_entity_id(self) -> str:
|
||||
"""The offset_calibration_entity_id"""
|
||||
return self._closing_degree_entity_id
|
||||
|
||||
@property
|
||||
def have_closing_degree_entity(self) -> bool:
|
||||
"""Return True if the underlying have a closing_degree entity"""
|
||||
return self._closing_degree_entity_id is not None
|
||||
|
||||
@property
|
||||
def have_offset_calibration_entity(self) -> bool:
|
||||
"""Return True if the underlying have a offset_calibration entity"""
|
||||
return self._offset_calibration_entity_id is not None
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""Get the hvac_modes"""
|
||||
if not self.is_initialized:
|
||||
return []
|
||||
return [HVACMode.OFF, HVACMode.HEAT]
|
||||
|
||||
@overrides
|
||||
async def start_cycle(
|
||||
self,
|
||||
hvac_mode: HVACMode,
|
||||
_1,
|
||||
_2,
|
||||
_3,
|
||||
force=False,
|
||||
):
|
||||
"""We use this function to change the on_percent"""
|
||||
# if force:
|
||||
await self.set_valve_open_percent()
|
||||
|
||||
@property
|
||||
def is_device_active(self):
|
||||
"""If the opening valve is open."""
|
||||
try:
|
||||
return get_safe_float(self._hass, self._opening_degree_entity_id) > 0
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
return False
|
||||
|
||||
@property
|
||||
def valve_entity_ids(self) -> [str]:
|
||||
"""get an arrary with all entityd id of the valve"""
|
||||
ret = []
|
||||
for entity in [
|
||||
self.opening_degree_entity_id,
|
||||
self.closing_degree_entity_id,
|
||||
self.offset_calibration_entity_id,
|
||||
]:
|
||||
if entity:
|
||||
ret.append(entity)
|
||||
return ret
|
||||
|
||||
@@ -179,7 +179,8 @@ class VersatileThermostatAPI(dict):
|
||||
# ):
|
||||
# await entity.init_presets(self.find_central_configuration())
|
||||
|
||||
# A little hack to test if the climate is a VTherm. Cannot use isinstance due to circular dependency of BaseThermostat
|
||||
# A little hack to test if the climate is a VTherm. Cannot use isinstance
|
||||
# due to circular dependency of BaseThermostat
|
||||
if (
|
||||
entity.device_info
|
||||
and entity.device_info.get("model", None) == DOMAIN
|
||||
@@ -249,6 +250,11 @@ class VersatileThermostatAPI(dict):
|
||||
"""Get the safety_mode params"""
|
||||
return self._safety_mode
|
||||
|
||||
@property
|
||||
def max_on_percent(self):
|
||||
"""Get the max_open_percent params"""
|
||||
return self._max_on_percent
|
||||
|
||||
@property
|
||||
def central_boiler_entity(self):
|
||||
"""Get the central boiler binary_sensor entity"""
|
||||
|
||||
211
documentation/en/additions.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# Some Essential Add-Ons
|
||||
|
||||
- [Some Essential Add-Ons](#some-essential-add-ons)
|
||||
- [the Versatile Thermostat UI Card](#the-versatile-thermostat-ui-card)
|
||||
- [the Scheduler Component!](#the-scheduler-component)
|
||||
- [Regulation curves with Plotly to Fine-Tune Your Thermostat](#regulation-curves-with-plotly-to-fine-tune-your-thermostat)
|
||||
- [Event notification with the AppDaemon NOTIFIER](#event-notification-with-the-appdaemon-notifier)
|
||||
|
||||
## the Versatile Thermostat UI Card
|
||||
A dedicated card for the Versatile Thermostat has been developed (based on Better Thermostat). It is available here: [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card) and offers a modern view of all the VTherm statuses:
|
||||
|
||||

|
||||
|
||||
## the Scheduler Component!
|
||||
|
||||
To make the most out of the Versatile Thermostat, I recommend using it with the [Scheduler Component](https://github.com/nielsfaber/scheduler-component). The scheduler component provides climate scheduling based on predefined modes. While this feature is somewhat limited with the generic thermostat, it becomes very powerful when paired with the Versatile Thermostat.
|
||||
|
||||
Assuming you have installed both the Versatile Thermostat and the Scheduler Component, here’s an example:
|
||||
|
||||
In Scheduler, add a schedule:
|
||||
|
||||

|
||||
|
||||
Choose the "Climate" group, select one (or more) entity, pick "MAKE SCHEME," and click next:
|
||||
(You can also choose "SET PRESET," but I prefer "MAKE SCHEME.")
|
||||
|
||||

|
||||
|
||||
Define your mode scheme and save:
|
||||
|
||||

|
||||
|
||||
In this example, I set ECO mode during the night and when no one is home during the day, BOOST in the morning, and COMFORT in the evening.
|
||||
|
||||
I hope this example helps; feel free to share your feedback!
|
||||
|
||||
## Regulation curves with Plotly to Fine-Tune Your Thermostat
|
||||
You can obtain a curve similar to the one shown in [some results](#some-results) using a Plotly graph configuration by leveraging the thermostat's custom attributes described [here](#custom-attributes):
|
||||
|
||||
Replace the values between `[[ ]]` with your own.
|
||||
<details>
|
||||
|
||||
```yaml
|
||||
- type: custom:plotly-graph
|
||||
entities:
|
||||
- entity: '[[climate]]'
|
||||
attribute: temperature
|
||||
yaxis: y1
|
||||
name: Consigne
|
||||
- entity: '[[climate]]'
|
||||
attribute: current_temperature
|
||||
yaxis: y1
|
||||
name: T°
|
||||
- entity: '[[climate]]'
|
||||
attribute: ema_temp
|
||||
yaxis: y1
|
||||
name: Ema
|
||||
- entity: '[[climate]]'
|
||||
attribute: on_percent
|
||||
yaxis: y2
|
||||
name: Power percent
|
||||
fill: tozeroy
|
||||
fillcolor: rgba(200, 10, 10, 0.3)
|
||||
line:
|
||||
color: rgba(200, 10, 10, 0.9)
|
||||
- entity: '[[slope]]'
|
||||
name: Slope
|
||||
fill: tozeroy
|
||||
yaxis: y9
|
||||
fillcolor: rgba(100, 100, 100, 0.3)
|
||||
line:
|
||||
color: rgba(100, 100, 100, 0.9)
|
||||
hours_to_show: 4
|
||||
refresh_interval: 10
|
||||
height: 800
|
||||
config:
|
||||
scrollZoom: true
|
||||
layout:
|
||||
margin:
|
||||
r: 50
|
||||
legend:
|
||||
x: 0
|
||||
'y': 1.2
|
||||
groupclick: togglegroup
|
||||
title:
|
||||
side: top right
|
||||
yaxis:
|
||||
visible: true
|
||||
position: 0
|
||||
yaxis2:
|
||||
visible: true
|
||||
position: 0
|
||||
fixedrange: true
|
||||
range:
|
||||
- 0
|
||||
- 1
|
||||
yaxis9:
|
||||
visible: true
|
||||
fixedrange: false
|
||||
range:
|
||||
- -2
|
||||
- 2
|
||||
position: 1
|
||||
xaxis:
|
||||
rangeselector:
|
||||
'y': 1.1
|
||||
x: 0.7
|
||||
buttons:
|
||||
- count: 1
|
||||
step: hour
|
||||
- count: 12
|
||||
step: hour
|
||||
- count: 1
|
||||
step: day
|
||||
- count: 7
|
||||
step: day
|
||||
```
|
||||
</details>
|
||||
|
||||
Example of curves obtained with Plotly:
|
||||
|
||||

|
||||
|
||||
## Event notification with the AppDaemon NOTIFIER
|
||||
This automation leverages the excellent AppDaemon app named NOTIFIER, developed by Horizon Domotique, demonstrated [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique), and the code is available [here](https://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows users to be notified of security-related events occurring on any Versatile Thermostat.
|
||||
|
||||
This is a great example of using the notifications described here: [notification](#notifications).
|
||||
<details>
|
||||
|
||||
```yaml
|
||||
alias: Surveillance Mode Sécurité chauffage
|
||||
description: Envoi une notification si un thermostat passe en mode sécurité ou power
|
||||
trigger:
|
||||
- platform: event
|
||||
event_type: versatile_thermostat_security_event
|
||||
id: versatile_thermostat_security_event
|
||||
- platform: event
|
||||
event_type: versatile_thermostat_power_event
|
||||
id: versatile_thermostat_power_event
|
||||
- platform: event
|
||||
event_type: versatile_thermostat_temperature_event
|
||||
id: versatile_thermostat_temperature_event
|
||||
condition: []
|
||||
action:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: versatile_thermostat_security_event
|
||||
sequence:
|
||||
- event: NOTIFIER
|
||||
event_data:
|
||||
action: send_to_jmc
|
||||
title: >-
|
||||
Radiateur {{ trigger.event.data.name }} - {{
|
||||
trigger.event.data.type }} Sécurité
|
||||
message: >-
|
||||
Le radiateur {{ trigger.event.data.name }} est passé en {{
|
||||
trigger.event.data.type }} sécurité car le thermomètre ne répond
|
||||
plus.\n{{ trigger.event.data }}
|
||||
callback:
|
||||
- title: Stopper chauffage
|
||||
event: stopper_chauffage
|
||||
image_url: /media/local/alerte-securite.jpg
|
||||
click_url: /lovelace-chauffage/4
|
||||
icon: mdi:radiator-off
|
||||
tag: radiateur_security_alerte
|
||||
persistent: true
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: versatile_thermostat_power_event
|
||||
sequence:
|
||||
- event: NOTIFIER
|
||||
event_data:
|
||||
action: send_to_jmc
|
||||
title: >-
|
||||
Radiateur {{ trigger.event.data.name }} - {{
|
||||
trigger.event.data.type }} Délestage
|
||||
message: >-
|
||||
Le radiateur {{ trigger.event.data.name }} est passé en {{
|
||||
trigger.event.data.type }} délestage car la puissance max est
|
||||
dépassée.\n{{ trigger.event.data }}
|
||||
callback:
|
||||
- title: Stopper chauffage
|
||||
event: stopper_chauffage
|
||||
image_url: /media/local/alerte-delestage.jpg
|
||||
click_url: /lovelace-chauffage/4
|
||||
icon: mdi:radiator-off
|
||||
tag: radiateur_power_alerte
|
||||
persistent: true
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: versatile_thermostat_temperature_event
|
||||
sequence:
|
||||
- event: NOTIFIER
|
||||
event_data:
|
||||
action: send_to_jmc
|
||||
title: >-
|
||||
Le thermomètre du radiateur {{ trigger.event.data.name }} ne
|
||||
répond plus
|
||||
message: >-
|
||||
Le thermomètre du radiateur {{ trigger.event.data.name }} ne
|
||||
répond plus depuis longtemps.\n{{ trigger.event.data }}
|
||||
image_url: /media/local/thermometre-alerte.jpg
|
||||
click_url: /lovelace-chauffage/4
|
||||
icon: mdi:radiator-disabled
|
||||
tag: radiateur_thermometre_alerte
|
||||
persistent: true
|
||||
mode: queued
|
||||
max: 30
|
||||
```
|
||||
</details>
|
||||
67
documentation/en/algorithms.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# The Different Algorithms Used
|
||||
|
||||
- [The Different Algorithms Used](#the-different-algorithms-used)
|
||||
- [The TPI Algorithm](#the-tpi-algorithm)
|
||||
- [Configuring the TPI Algorithm Coefficients](#configuring-the-tpi-algorithm-coefficients)
|
||||
- [Principle](#principle)
|
||||
- [The Self-Regulation Algorithm (Without Valve Control)](#the-self-regulation-algorithm-without-valve-control)
|
||||
- [The Auto-Start/Stop Function Algorithm](#the-auto-startstop-function-algorithm)
|
||||
|
||||
## The TPI Algorithm
|
||||
|
||||
### Configuring the TPI Algorithm Coefficients
|
||||
|
||||
If you have selected a thermostat of type `over_switch`, `over_valve`, or `over_climate` with self-regulation in `Direct Valve Control` mode and choose the "TPI" option in the menu, you will land on this page:
|
||||
|
||||

|
||||
|
||||
You need to provide:
|
||||
1. the coefficient `coef_int` for the TPI algorithm,
|
||||
2. the coefficient `coef_ext` for the TPI algorithm.
|
||||
|
||||
### Principle
|
||||
|
||||
The TPI algorithm calculates the On vs Off percentage for the radiator at each cycle, using the target temperature, the current room temperature, and the current outdoor temperature. This algorithm is only applicable for Versatile Thermostats operating in `over_switch` and `over_valve` modes.
|
||||
|
||||
The percentage is calculated using this formula:
|
||||
|
||||
on_percent = coef_int * (target_temperature - current_temperature) + coef_ext * (target_temperature - outdoor_temperature)
|
||||
Then, the algorithm ensures that 0 <= on_percent <= 1.
|
||||
|
||||
The default values for `coef_int` and `coef_ext` are `0.6` and `0.01`, respectively. These default values are suitable for a standard well-insulated room.
|
||||
|
||||
When adjusting these coefficients, keep the following in mind:
|
||||
1. **If the target temperature is not reached** after stabilization, increase `coef_ext` (the `on_percent` is too low),
|
||||
2. **If the target temperature is exceeded** after stabilization, decrease `coef_ext` (the `on_percent` is too high),
|
||||
3. **If reaching the target temperature is too slow**, increase `coef_int` to provide more power to the heater,
|
||||
4. **If reaching the target temperature is too fast and oscillations occur** around the target, decrease `coef_int` to provide less power to the radiator.
|
||||
|
||||
In `over_valve` mode, the `on_percent` value is converted to a percentage (0 to 100%) and directly controls the valve's opening level.
|
||||
|
||||
## The Self-Regulation Algorithm (Without Valve Control)
|
||||
|
||||
The self-regulation algorithm can be summarized as follows:
|
||||
|
||||
1. Initialize the target temperature as the VTherm setpoint,
|
||||
2. If self-regulation is enabled:
|
||||
1. Calculate the regulated temperature (valid for a VTherm),
|
||||
2. Use this temperature as the target,
|
||||
3. For each underlying device of the VTherm:
|
||||
1. If "Use Internal Temperature" is checked:
|
||||
1. Calculate the compensation (`trv_internal_temp - room_temp`),
|
||||
2. Add the offset to the target temperature,
|
||||
3. Send the target temperature (= regulated_temp + (internal_temp - room_temp)) to the underlying device.
|
||||
|
||||
## The Auto-Start/Stop Function Algorithm
|
||||
|
||||
The algorithm used in the auto-start/stop function operates as follows:
|
||||
1. If "Enable Auto-Start/Stop" is off, stop here.
|
||||
2. If VTherm is on and in Heating mode, when `error_accumulated` < `-error_threshold` -> turn off and save HVAC mode.
|
||||
3. If VTherm is on and in Cooling mode, when `error_accumulated` > `error_threshold` -> turn off and save HVAC mode.
|
||||
4. If VTherm is off and the saved HVAC mode is Heating, and `current_temperature + slope * dt <= target_temperature`, turn on and set the HVAC mode to the saved mode.
|
||||
5. If VTherm is off and the saved HVAC mode is Cooling, and `current_temperature + slope * dt >= target_temperature`, turn on and set the HVAC mode to the saved mode.
|
||||
6. `error_threshold` is set to `10 (° * min)` for slow detection, `5` for medium, and `2` for fast.
|
||||
|
||||
`dt` is set to `30 min` for slow, `15 min` for medium, and `7 min` for fast detection levels.
|
||||
|
||||
The function is detailed [here](https://github.com/jmcollin78/versatile_thermostat/issues/585).
|
||||
45
documentation/en/base-attributes.md
Normal file
@@ -0,0 +1,45 @@
|
||||
- [Choosing Basic Attributes](#choosing-basic-attributes)
|
||||
- [Choosing the features to Use](#choosing-the-features-to-use)
|
||||
|
||||
# Choosing Basic Attributes
|
||||
|
||||
Select the "Main Attributes" menu.
|
||||
|
||||

|
||||
|
||||
Provide the mandatory main attributes. These attributes are common to all VTherms:
|
||||
1. A name (this will be both the integration's name and the `climate` entity name),
|
||||
2. An entity ID of a temperature sensor that provides the room temperature where the radiator is installed,
|
||||
3. An optional sensor entity providing the last seen date and time of the sensor (`last_seen`). If available, specify it here. It helps prevent safety shutdowns when the temperature is stable, and the sensor stops reporting for a long time (see [here](troubleshooting.md#why-does-my-versatile-thermostat-go-into-safety-mode)),
|
||||
4. A cycle duration in minutes. At each cycle:
|
||||
1. For `over_switch`: VTherm will turn the radiator on/off, modulating the proportion of time it is on,
|
||||
2. For `over_valve`: VTherm will calculate a new valve opening level and send it if it has changed,
|
||||
3. For `over_climate`: The cycle performs basic controls and recalculates the self-regulation coefficients. The cycle may result in a new setpoint sent to underlying devices or a valve opening adjustment in the case of a controllable TRV.
|
||||
5. The equipment's power, which will activate power and energy consumption sensors for the device. If multiple devices are linked to the same VTherm, specify the total maximum power of all devices here,
|
||||
6. The option to use additional parameters from centralized configuration:
|
||||
1. Outdoor temperature sensor,
|
||||
2. Minimum/maximum temperature and temperature step size,
|
||||
7. The option to control the thermostat centrally. See [centralized control](#centralized-control),
|
||||
8. A checkbox if this VTherm is used to trigger a central boiler.
|
||||
|
||||
>  _*Notes*_
|
||||
> 1. With the `over_switch` and `over_valve` types, calculations are performed at each cycle. In case of changing conditions, you will need to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 minutes is a good value**, but it should be adjusted to your heating type. The greater the inertia, the longer the cycle should be. See [Tuning examples](tuning-examples.md).
|
||||
> 2. If the cycle is too short, the radiator may never reach the target temperature. For example, with a storage heater, it will be unnecessarily activated.
|
||||
|
||||
# Choosing the features to Use
|
||||
|
||||
Select the "Features" menu.
|
||||
|
||||

|
||||
|
||||
Choose the features you want to use for this VTherm:
|
||||
1. **Opening detection** (doors, windows) stops heating when an opening is detected. (see [managing openings](feature-window.md)),
|
||||
2. **Motion detection**: VTherm can adjust the target temperature when motion is detected in the room. (see [motion detection](feature-motion.md)),
|
||||
3. **Power management**: VTherm can stop a device if the power consumption in your home exceeds a threshold. (see [load-shedding management](feature-power.md)),
|
||||
4. **Presence detection**: If you have a sensor indicating presence or absence in your home, you can use it to change the target temperature. See [presence management](feature-presence.md). Note the difference between this function and motion detection: presence is typically used at the home level, while motion detection is more room-specific.
|
||||
5. **Automatic start/stop**: For `over_climate` VTherms only. This function stops a device when VTherm detects it will not be needed for a while. It uses the temperature curve to predict when the device will be needed again and turns it back on at that time. See [automatic start/stop management](feature-auto-start-stop.md).
|
||||
|
||||
>  _*Notes*_
|
||||
> 1. The list of available functions adapts to your VTherm type.
|
||||
> 2. When you enable a function, a new menu entry is added to configure it.
|
||||
> 3. You cannot validate the creation of a VTherm if all parameters for all enabled functions have not been configured.
|
||||
71
documentation/en/creation.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Choosing a VTherm
|
||||
|
||||
- [Choosing a VTherm](#choosing-a-vtherm)
|
||||
- [Creating a New Versatile Thermostat](#creating-a-new-versatile-thermostat)
|
||||
- [Choosing a VTherm Type](#choosing-a-vtherm-type)
|
||||
- [Centralized configuration](#centralized-configuration)
|
||||
- [VTherm over a switch](#vtherm-over-a-switch)
|
||||
- [VTherm over another thermostat](#vtherm-over-another-thermostat)
|
||||
- [VTherm over a valve](#vtherm-over-a-valve)
|
||||
- [Making the right choice](#making-the-right-choice)
|
||||
- [Reference Article](#reference-article)
|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> There are three ways to work with VTherms:
|
||||
> 1. Each Versatile Thermostat is fully configured independently. Choose this option if you do not want any centralized configuration or management.
|
||||
> 2. Some aspects are configured centrally. For example, you can define the minimum/maximum temperatures, open window detection parameters, etc., at a single central instance. For each VTherm you configure, you can then choose to use the central configuration or override it with custom parameters.
|
||||
> 3. In addition to centralized configuration, all VTherms can be controlled by a single `select` entity called `central_mode`. This feature allows you to stop/start/set frost protection/etc. for all VTherms at once. For each VTherm, you can specify if it is affected by this `central_mode`.
|
||||
|
||||
## Creating a New Versatile Thermostat
|
||||
|
||||
Click on "Add Integration" on the integration page (or click 'Add device' in the integration page)
|
||||
|
||||

|
||||
|
||||
then:
|
||||
|
||||

|
||||
|
||||
The configuration can be modified via the same interface. Simply select the thermostat to modify, press "Configure," and you will be able to change some parameters or settings.
|
||||
|
||||
Follow the configuration steps by selecting the menu option to configure.
|
||||
|
||||
# Choosing a VTherm Type
|
||||
|
||||
## Centralized configuration
|
||||
This option allows you to configure certain repetitive aspects for all VTherms at once, such as:
|
||||
1. Parameters for different algorithms (TPI, open window detection, motion detection, power sensors for your home, presence detection). These parameters apply across all VTherms. You only need to enter them once in `Centralized Configuration`. This configuration does not create a VTherm itself but centralizes parameters that would be tedious to re-enter for each VTherm. Note that you can override these parameters on individual VTherms to specialize them if needed.
|
||||
2. Configuration for controlling a central heating system,
|
||||
3. Certain advanced parameters, such as safety settings.
|
||||
|
||||
## VTherm over a switch
|
||||
This VTherm type controls a switch that turns a radiator on or off. The switch can be a physical switch directly controlling a radiator (often electric) or a virtual switch that can perform any action when turned on or off. The latter type can, for example, control pilot wire switches or DIY pilot wire solutions with diodes. VTherm modulates the proportion of time the radiator is on (`on_percent`) to achieve the desired temperature. If it is cold, it turns on more frequently (up to 100%); if it is warm, it reduces the on time.
|
||||
|
||||
The underlying entities for this type are `switches` or `input_booleans`.
|
||||
|
||||
## VTherm over another thermostat
|
||||
When your device is controlled by a `climate` entity in Home Assistant and you only have access to this, you should use this VTherm type. In this case, VTherm simply adjusts the target temperature of the underlying `climate` entity.
|
||||
|
||||
This type also includes advanced self-regulation features to adjust the setpoint sent to the underlying device, helping to achieve the target temperature faster and mitigating poor internal regulation. For example, if the device's internal thermometer is too close to the heating element, it may incorrectly assume the room is warm while the setpoint is far from being achieved in other areas.
|
||||
|
||||
Since version 6.8, this VTherm type can also regulate directly by controlling the valve. Ideal for controllable TRVs, this type is recommended if you have such devices.
|
||||
|
||||
The underlying entities for this VTherm type are exclusively `climate`.
|
||||
|
||||
## VTherm over a valve
|
||||
If the only entity available to regulate your radiator's temperature is a `number` entity, you should use the `over_valve` type. VTherm adjusts the valve opening based on the difference between the target temperature and the actual room temperature (and the outdoor temperature, if available).
|
||||
|
||||
This type can be used for TRVs without an associated `climate` entity or other DIY solutions exposing a `number` entity.
|
||||
|
||||
# Making the right choice
|
||||
>  _*How to Choose the Type*_
|
||||
> Choosing the correct type is crucial. It cannot be changed later via the configuration interface. To make the right choice, consider the following questions:
|
||||
> 1. **What type of equipment will I control?** Follow this order of preference:
|
||||
> 1. If you have a controllable thermostatic valve (TRV) in Home Assistant through a `number` entity (e.g., a Shelly TRV), choose the `over_valve` type. This is the most direct type and ensures the best regulation.
|
||||
> 2. If you have an electric radiator (with or without a pilot wire) controlled by a `switch` entity to turn it on/off, then the `over_switch` type is preferable. Regulation will be managed by the Versatile Thermostat based on the temperature measured by your thermometer at its placement location.
|
||||
> 3. In all other cases, use the `over_climate` mode. You retain your original `climate` entity, and the Versatile Thermostat "only" controls the on/off state and target temperature of your original thermostat. Regulation is handled by your original thermostat in this case. This mode is particularly suited for all-in-one reversible air conditioning systems exposed as a `climate` entity in Home Assistant. Advanced self-regulation can achieve the setpoint faster by forcing the setpoint or directly controlling the valve when possible.
|
||||
> 2. **What type of regulation do I want?** If the controlled equipment has its own built-in regulation mechanism (e.g., HVAC systems, certain TRVs) and it works well, choose `over_climate`. For TRVs with a controllable valve in Home Assistant, the `over_climate` type with `Direct Valve Control` self-regulation is the best choice.
|
||||
|
||||
# Reference Article
|
||||
For more information on these concepts, refer to this article (in French): https://www.hacf.fr/optimisation-versatile-thermostat/#optimiser-vos-vtherm
|
||||
53
documentation/en/feature-advanced.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Advanced Configuration
|
||||
|
||||
- [Advanced Configuration](#advanced-configuration)
|
||||
- [Advanced Settings](#advanced-settings)
|
||||
- [Minimum Activation Delay](#minimum-activation-delay)
|
||||
- [Safety Mode](#safety-mode)
|
||||
|
||||
These settings refine the thermostat's operation, particularly the safety mechanism for a _VTherm_. Missing temperature sensors (room or outdoor) can pose a risk to your home. For instance, if the temperature sensor gets stuck at 10°C, the `over_climate` or `over_valve` _VTherm_ types will command maximum heating of the underlying devices, which could lead to room overheating or even property damage, at worst resulting in a fire hazard.
|
||||
|
||||
To prevent this, _VTherm_ ensures that thermometers report values regularly. If they don't, the _VTherm_ switches to a special mode called Safety Mode. This mode ensures minimal heating to prevent the opposite risk: a completely unheated home in the middle of winter, for example.
|
||||
|
||||
The challenge lies in that some thermometers—especially battery-operated ones—only send temperature updates when the value changes. It is entirely possible to receive no temperature updates for hours without the thermometer failing. The parameters below allow fine-tuning of the thresholds for activating Safety Mode.
|
||||
|
||||
If your thermometer has a `last seen` attribute indicating the last contact time, you can specify it in the _VTherm_'s main attributes to greatly reduce false Safety Mode activations. See [configuration](base-attributes.md#choosing-base-attributes) and [troubleshooting](troubleshooting.md#why-does-my-versatile-thermostat-switch-to-safety-mode).
|
||||
|
||||
For `over_climate` _VTherms_, which self-regulate, Safety Mode is disabled. In this case, there is no danger, only the risk of an incorrect temperature.
|
||||
|
||||
## Advanced Settings
|
||||
|
||||
The advanced configuration form looks like this:
|
||||
|
||||

|
||||
|
||||
### Minimum Activation Delay
|
||||
|
||||
The first delay (`minimal_activation_delay_sec`) in seconds is the minimum acceptable delay to turn on the heating. If the calculated activation time is shorter than this value, the heating remains off. This parameter only applies to _VTherm_ with cyclic triggering `over_switch`. If the activation time is too short, rapid switching will not allow the device to heat up properly.
|
||||
|
||||
### Safety Mode
|
||||
|
||||
The second delay (`security_delay_min`) is the maximum time between two temperature measurements before the _VTherm_ switches to Safety Mode.
|
||||
|
||||
The third parameter (`security_min_on_percent`) is the minimum `on_percent` below which Safety Mode will not be activated. This setting prevents activating Safety Mode if the controlled radiator does not heat sufficiently. In this case, there is no physical risk to the home, only the risk of overheating or underheating.
|
||||
Setting this parameter to `0.00` will trigger Safety Mode regardless of the last heating setting, whereas `1.00` will never trigger Safety Mode (effectively disabling the feature). This can be useful to adapt the safety mechanism to your specific needs.
|
||||
|
||||
The fourth parameter (`security_default_on_percent`) defines the `on_percent` used when the thermostat switches to `security` mode. Setting it to `0` will turn off the thermostat in Safety Mode, while setting it to a value like `0.2` (20%) ensures some heating remains, avoiding a completely frozen home in case of a thermometer failure.
|
||||
|
||||
It is possible to disable Safety Mode triggered by missing data from the outdoor thermometer. Since the outdoor thermometer usually has a minor impact on regulation (depending on your configuration), it might not be critical if it's unavailable. To do this, add the following lines to your `configuration.yaml`:
|
||||
|
||||
```yaml
|
||||
versatile_thermostat:
|
||||
...
|
||||
safety_mode:
|
||||
check_outdoor_sensor: false
|
||||
```
|
||||
|
||||
By default, the outdoor thermometer can trigger Safety Mode if it stops sending data. Remember that Home Assistant must be restarted for these changes to take effect. This setting applies to all _VTherms_ sharing the outdoor thermometer.
|
||||
|
||||
>  _*Notes*_
|
||||
> 1. When the temperature sensor resumes reporting, the preset will be restored to its previous value.
|
||||
> 2. Two temperature sources are required: the indoor and outdoor temperatures. Both must report values, or the thermostat will switch to "security" preset.
|
||||
> 3. An action is available to adjust the three safety parameters. This can help adapt Safety Mode to your needs.
|
||||
> 4. For normal use, `security_default_on_percent` should be lower than `security_min_on_percent`.
|
||||
> 5. If you use the Versatile Thermostat UI card (see [here](additions.md#better-with-the-versatile-thermostat-ui-card)), a _VTherm_ in Safety Mode is indicated by a gray overlay showing the faulty thermometer and the time since its last value update: .
|
||||
39
documentation/en/feature-auto-start-stop.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Auto-start / Auto-stop
|
||||
|
||||
- [Auto-start / Auto-stop](#auto-start--auto-stop)
|
||||
- [Configure Auto-start/stop](#configure-auto-startstop)
|
||||
- [Usage](#usage)
|
||||
|
||||
This feature allows _VTherm_ to stop an appliance that doesn't need to be on and restart it when conditions require it. This function includes three settings that control how quickly the appliance is stopped and restarted.
|
||||
Exclusively reserved for _VTherm_ of type `over_climate`, it applies to the following use case:
|
||||
1. Your appliance is permanently powered on and consumes electricity even when heating (or cooling) is not needed. This is often the case with heat pumps (_PAC_) that consume power even in standby mode.
|
||||
2. The temperature conditions are such that heating (or cooling) is not needed for a long period: the setpoint is higher (or lower) than the room temperature.
|
||||
3. The temperature rises (or falls), remains stable, or falls (or rises) slowly.
|
||||
|
||||
In such cases, it is preferable to ask the appliance to turn off to avoid unnecessary power consumption in standby mode.
|
||||
|
||||
## Configure Auto-start/stop
|
||||
|
||||
To use this feature, you need to:
|
||||
1. Add the `With auto-start and stop` function in the 'Functions' menu.
|
||||
2. Set the detection level in the 'Auto-start/stop' option that appears when the function is activated. Choose the detection level between 'Slow', 'Medium', and 'Fast'. With the 'Fast' setting, stops and restarts will occur more frequently.
|
||||
|
||||

|
||||
|
||||
The 'Slow' setting allows about 30 minutes between a stop and a restart,
|
||||
The 'Medium' setting sets the threshold to about 15 minutes, and the 'Fast' setting puts it at 7 minutes.
|
||||
|
||||
Note that these are not absolute settings since the algorithm takes into account the slope of the room temperature curve to respond accordingly. It is still possible that a restart occurs shortly after a stop if the temperature drops significantly.
|
||||
|
||||
## Usage
|
||||
|
||||
Once the function is configured, you will now have a new `switch` type entity that allows you to enable or disable auto-start/stop without modifying the configuration. This entity is available on the _VTherm_ device and is named `switch.<name>_enable_auto_start_stop`.
|
||||
|
||||

|
||||
|
||||
Check the box to allow auto-start and auto-stop, and leave it unchecked to disable the feature.
|
||||
|
||||
>  _*Notes*_
|
||||
> 1. The detection algorithm is described [here](algorithms.md#auto-startstop-algorithm).
|
||||
> 2. Some appliances (boilers, underfloor heating, _PAC_, etc.) may not like being started/stopped too frequently. If that's the case, it might be better to disable the function when you know the appliance will be used. For example, I disable this feature during the day when presence is detected because I know my _PAC_ will turn on often. I enable auto-start/stop at night or when no one is home, as the setpoint is lowered and it rarely triggers.
|
||||
> 3. If you use the Versatile Thermostat UI card (see [here](additions.md#better-with-the-versatile-thermostat-ui-card)), a checkbox is directly visible on the card to disable auto-start/stop, and a _VTherm_ stopped by auto-start/stop is indicated by the icon: .
|
||||
123
documentation/en/feature-central-boiler.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Le contrôle d'une chaudière centrale# Controlling a Central Boiler
|
||||
|
||||
- [Le contrôle d'une chaudière centrale# Controlling a Central Boiler](#le-contrôle-dune-chaudière-centrale-controlling-a-central-boiler)
|
||||
- [Principle](#principle)
|
||||
- [Configuration](#configuration)
|
||||
- [How to Find the Right Action?](#how-to-find-the-right-action)
|
||||
- [Events](#events)
|
||||
- [Warning](#warning)
|
||||
|
||||
You can control a centralized boiler. As long as it's possible to trigger or stop the boiler from Home Assistant, Versatile Thermostat will be able to control it directly.
|
||||
|
||||
## Principle
|
||||
The basic principle is as follows:
|
||||
1. A new entity of type `binary_sensor`, named by default `binary_sensor.central_boiler`, is added.
|
||||
2. In the configuration of the _VTherms_, you specify whether the _VTherm_ should control the boiler. In a heterogeneous installation, some _VTherms_ should control the boiler, and others should not. Therefore, you need to indicate in each _VTherm_ configuration whether it controls the boiler.
|
||||
3. The `binary_sensor.central_boiler` listens for state changes in the equipment of the _VTherms_ marked as controlling the boiler.
|
||||
4. When the number of devices controlled by the _VTherm_ requesting heating (i.e., when its `hvac_action` changes to `Heating`) exceeds a configurable threshold, the `binary_sensor.central_boiler` turns `on`, and **if an activation service has been configured, that service is called**.
|
||||
5. If the number of devices requesting heating drops below the threshold, the `binary_sensor.central_boiler` turns `off`, and **if a deactivation service has been configured, that service is called**.
|
||||
6. You have access to two entities:
|
||||
- A `number` type entity, named by default `number.boiler_activation_threshold`, which gives the activation threshold. This threshold is the number of devices (radiators) requesting heating.
|
||||
- A `sensor` type entity, named by default `sensor.nb_device_active_for_boiler`, which shows the number of devices requesting heating. For example, a _VTherm_ with 4 valves, 3 of which request heating, will make this sensor show 3. Only the devices from _VTherms_ marked to control the central boiler are counted.
|
||||
|
||||
You therefore always have the information to manage and adjust the triggering of the boiler.
|
||||
|
||||
All these entities are linked to the central configuration service:
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
To configure this feature, you need a centralized configuration (see [Configuration](#configuration)) and check the 'Add Central Boiler' box:
|
||||
|
||||

|
||||
|
||||
On the next page, you can provide the configuration for the actions (e.g., services) to be called when the boiler is turned on/off:
|
||||
|
||||

|
||||
|
||||
The actions (e.g., services) are configured as described on the page:
|
||||
1. The general format is `entity_id/service_id[/attribute:value]` (where `/attribute:value` is optional).
|
||||
2. `entity_id` is the name of the entity controlling the boiler in the form `domain.entity_name`. For example: `switch.chaudiere` for a boiler controlled by a switch, or `climate.chaudière` for a boiler controlled by a thermostat, or any other entity that allows boiler control (there is no limitation). You can also toggle inputs (`helpers`) such as `input_boolean` or `input_number`.
|
||||
3. `service_id` is the name of the service to be called in the form `domain.service_name`. For example: `switch.turn_on`, `switch.turn_off`, `climate.set_temperature`, `climate.set_hvac_mode` are valid examples.
|
||||
4. Some services require a parameter. This could be the 'HVAC Mode' for `climate.set_hvac_mode` or the target temperature for `climate.set_temperature`. This parameter should be configured in the format `attribute:value` at the end of the string.
|
||||
|
||||
Examples (to adjust to your case):
|
||||
- `climate.chaudiere/climate.set_hvac_mode/hvac_mode:heat`: to turn the boiler thermostat on in heating mode.
|
||||
- `climate.chaudiere/climate.set_hvac_mode/hvac_mode:off`: to turn off the boiler thermostat.
|
||||
- `switch.pompe_chaudiere/switch.turn_on`: to turn on the switch powering the boiler pump.
|
||||
- `switch.pompe_chaudiere/switch.turn_off`: to turn off the switch powering the boiler pump.
|
||||
- ...
|
||||
|
||||
### How to Find the Right Action?
|
||||
To find the correct action to use, it's best to go to "Developer Tools / Services", search for the action to call, the entity to control, and any required parameters.
|
||||
Click 'Call Service'. If your boiler turns on, you have the correct configuration. Then switch to YAML mode and copy the parameters.
|
||||
|
||||
Example:
|
||||
|
||||
In "Developer Tools / Actions":
|
||||
|
||||

|
||||
|
||||
In YAML mode:
|
||||
|
||||

|
||||
|
||||
The service to configure will then be: `climate.sonoff/climate.set_hvac_mode/hvac_mode:heat` (note the removal of spaces in `hvac_mode:heat`).
|
||||
|
||||
Do the same for the off service, and you’re ready to go.
|
||||
|
||||
## Events
|
||||
|
||||
Each successful boiler activation or deactivation sends an event from Versatile Thermostat. This can be captured by an automation, for example, to notify you of the change.
|
||||
The events look like this:
|
||||
|
||||
An activation event:
|
||||
```yaml
|
||||
event_type: versatile_thermostat_central_boiler_event
|
||||
data:
|
||||
central_boiler: true
|
||||
entity_id: binary_sensor.central_boiler
|
||||
name: Central boiler
|
||||
state_attributes: null
|
||||
origin: LOCAL
|
||||
time_fired: "2024-01-14T11:33:52.342026+00:00"
|
||||
context:
|
||||
id: 01HM3VZRJP3WYYWPNSDAFARW1T
|
||||
parent_id: null
|
||||
user_id: null
|
||||
```yaml
|
||||
event_type: versatile_thermostat_central_boiler_event
|
||||
data:
|
||||
central_boiler: true
|
||||
entity_id: binary_sensor.central_boiler
|
||||
name: Central boiler
|
||||
state_attributes: null
|
||||
origin: LOCAL
|
||||
time_fired: "2024-01-14T11:33:52.342026+00:00"
|
||||
context:
|
||||
id: 01HM3VZRJP3WYYWPNSDAFARW1T
|
||||
parent_id: null
|
||||
user_id: null
|
||||
```
|
||||
|
||||
Un évènement d'extinction :
|
||||
```yaml
|
||||
event_type: versatile_thermostat_central_boiler_event
|
||||
data:
|
||||
central_boiler: false
|
||||
entity_id: binary_sensor.central_boiler
|
||||
name: Central boiler
|
||||
state_attributes: null
|
||||
origin: LOCAL
|
||||
time_fired: "2024-01-14T11:43:52.342026+00:00"
|
||||
context:
|
||||
id: 01HM3VZRJP3WYYWPNSDAFBRW1T
|
||||
parent_id: null
|
||||
user_id: null
|
||||
```
|
||||
|
||||
## Warning
|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> Software or home automation control of a central boiler may pose risks to its proper operation. Before using these functions, ensure that your boiler has proper safety features and that they are functioning correctly. For example, turning on a boiler with all valves closed can create excessive pressure.
|
||||
31
documentation/en/feature-central-mode.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Centralized Control
|
||||
|
||||
- [Centralized Control](#centralized-control)
|
||||
- [Configuration of Centralized Control](#configuration-of-centralized-control)
|
||||
- [Usage](#usage)
|
||||
|
||||
This feature allows you to control all your _VTherms_ from a single control point.
|
||||
A typical use case is when you leave for an extended period and want to set all your _VTherms_ to frost protection, and when you return, you want to set them back to their initial state.
|
||||
|
||||
Centralized control is done from a special _VTherm_ called centralized configuration. See [here](creation.md#centralized-configuration) for more information.
|
||||
|
||||
## Configuration of Centralized Control
|
||||
|
||||
If you have set up a centralized configuration, you will have a new entity named `select.central_mode` that allows you to control all _VTherms_ with a single action.
|
||||
|
||||

|
||||
|
||||
This entity appears as a list of choices containing the following options:
|
||||
1. `Auto`: the 'normal' mode where each _VTherm_ operates autonomously,
|
||||
2. `Stopped`: all _VTherms_ are turned off (`hvac_off`),
|
||||
3. `Heat only`: all _VTherms_ are set to heating mode if supported, otherwise they are stopped,
|
||||
4. `Cool only`: all _VTherms_ are set to cooling mode if supported, otherwise they are stopped,
|
||||
5. `Frost protection`: all _VTherms_ are set to frost protection mode if supported, otherwise they are stopped.
|
||||
|
||||
## Usage
|
||||
|
||||
For a _VTherm_ to be controllable centrally, its configuration attribute named `use_central_mode` must be true. This attribute is available in the configuration page `Main Attributes`.
|
||||
|
||||

|
||||
|
||||
This means you can control all _VTherms_ (those explicitly designated) with a single control.
|
||||
40
documentation/en/feature-motion.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Motion or Activity Detection
|
||||
|
||||
- [Motion or Activity Detection](#motion-or-activity-detection)
|
||||
- [Configure Activity Mode or Motion Detection](#configure-activity-mode-or-motion-detection)
|
||||
- [Usage](#usage)
|
||||
|
||||
This feature allows you to change presets when motion is detected in a room. If you don't want to heat your office when the room is occupied and only when the room is occupied, you need a motion (or presence) sensor in the room and configure this feature.
|
||||
|
||||
This function is often confused with the presence feature. They are complementary but not interchangeable. The 'motion' function is local to a room equipped with a motion sensor, while the 'presence' function is designed to be global to the entire home.
|
||||
|
||||
## Configure Activity Mode or Motion Detection
|
||||
|
||||
If you have chosen the `With motion detection` feature:
|
||||
|
||||

|
||||
|
||||
What we need:
|
||||
- a **motion sensor**. Entity ID of a motion sensor. The states of the motion sensor must be "on" (motion detected) or "off" (no motion detected),
|
||||
- a **detection delay** (in seconds) defining how long we wait for confirmation of the motion before considering the motion. This parameter can be **greater than your motion sensor's delay**, otherwise, the detection will happen with every motion detected by the sensor,
|
||||
- an **inactivity delay** (in seconds) defining how long we wait for confirmation of no motion before no longer considering the motion,
|
||||
- a **"motion" preset**. We will use the temperature of this preset when activity is detected,
|
||||
- a **"no motion" preset**. We will use the temperature of this second preset when no activity is detected.
|
||||
|
||||
## Usage
|
||||
|
||||
To tell a _VTherm_ that it should listen to the motion sensor, you must set it to the special 'Activity' preset. If you have installed the Versatile Thermostat UI card (see [here](additions.md#much-better-with-the-versatile-thermostat-ui-card)), this preset is displayed as follows: .
|
||||
|
||||
You can then, upon request, set a _VTherm_ to motion detection mode.
|
||||
|
||||
The behavior will be as follows:
|
||||
- we have a room with a thermostat set to activity mode, the "motion" mode chosen is comfort (21.5°C), the "no motion" mode chosen is Eco (18.5°C), and the motion delay is 30 seconds on detection and 5 minutes on the end of detection.
|
||||
- the room has been empty for a while (no activity detected), the setpoint temperature in this room is 18.5°.
|
||||
- someone enters the room, and activity is detected if the motion is present for at least 30 seconds. The temperature then goes up to 21.5°.
|
||||
- if the motion is present for less than 30 seconds (quick passage), the temperature stays at 18.5°.
|
||||
- imagine the temperature has gone up to 21.5°, when the person leaves the room, after 5 minutes the temperature is returned to 18.5°.
|
||||
- if the person returns before the 5 minutes, the temperature stays at 21.5°.
|
||||
|
||||
>  _*Notes*_
|
||||
> 1. As with other presets, `Activity` will only be offered if it is correctly configured. In other words, all 4 configuration keys must be set.
|
||||
> 2. If you are using the Versatile Thermostat UI card (see [here](additions.md#much-better-with-the-versatile-thermostat-ui-card)), motion detection is represented as follows: .
|
||||
46
documentation/en/feature-power.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Power Management - Load Shedding
|
||||
|
||||
- [Power Management - Load Shedding](#power-management---load-shedding)
|
||||
- [Configure Power Management](#configure-power-management)
|
||||
|
||||
This feature allows you to regulate the electricity consumption of your heaters. Known as load shedding, this feature enables you to limit the electrical consumption of your heating device if overcapacity conditions are detected.
|
||||
You will need a **sensor for the total instantaneous power consumption** of your home, as well as a **sensor for the maximum allowed power**.
|
||||
|
||||
The behavior of this feature is basic:
|
||||
1. when the _VTherm_ is about to turn on a device,
|
||||
2. it compares the last known value of the power consumption sensor with the last value of the maximum allowed power. If there is a remaining margin greater than or equal to the declared power of the _VTherm_'s devices, then the _VTherm_ and its devices will be turned on. Otherwise, they will remain off until the next cycle.
|
||||
|
||||
WARNING: This very basic operation **is not a safety function** but more of an optimization feature to manage consumption at the cost of heating performance. Overloads may occur depending on the frequency of updates from your consumption sensors, and the actual power used by your devices. Therefore, you must always maintain a safety margin.
|
||||
|
||||
Typical use case:
|
||||
1. you have an electricity meter limited to 11 kW,
|
||||
2. you occasionally charge an electric vehicle at 5 kW,
|
||||
3. that leaves 6 kW for everything else, including heating,
|
||||
4. you have 1 kW of other equipment running,
|
||||
5. you have declared a sensor (`input_number`) for the maximum allowed power at 9 kW (= 11 kW - the reserve for other devices - margin)
|
||||
|
||||
If the vehicle is charging, the total power consumed is 6 kW (5+1), and a _VTherm_ will only turn on if its declared power is 3 kW max (9 kW - 6 kW).
|
||||
If the vehicle is charging and another _VTherm_ of 2 kW is running, the total power consumed is 8 kW (5+1+2), and a _VTherm_ will only turn on if its declared power is 1 kW max (9 kW - 8 kW). Otherwise, it will wait until the next cycle.
|
||||
|
||||
If the vehicle is not charging, the total power consumed is 1 kW, and a _VTherm_ will only turn on if its declared power is 8 kW max (9 kW - 1 kW).
|
||||
|
||||
## Configure Power Management
|
||||
|
||||
If you have chosen the `With power detection` feature, configure it as follows:
|
||||
|
||||

|
||||
|
||||
1. the entity ID of the **instantaneous power consumption sensor** for your home,
|
||||
2. the entity ID of the **maximum allowed power sensor**,
|
||||
3. the temperature to apply if load shedding is activated.
|
||||
|
||||
Note that all power values must have the same units (kW or W, for example).
|
||||
Having a **maximum allowed power sensor** allows you to adjust the maximum power over time using a scheduler or automation.
|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> 1. In case of load shedding, the radiator is set to the preset named `power`. This is a hidden preset, and you cannot select it manually.
|
||||
> 2. Always keep a margin, as the maximum power may briefly be exceeded while waiting for the next cycle calculation, or due to unregulated equipment.
|
||||
> 3. If you don't want to use this feature, uncheck it in the 'Functions' menu.
|
||||
> 4. If a _VTherm_ controls multiple devices, the **electrical consumption of your heating** must match the sum of the powers.
|
||||
> 5. If you are using the Versatile Thermostat UI card (see [here](additions.md#much-better-with-the-versatile-thermostat-ui-card)), load shedding is represented as follows: .
|
||||
24
documentation/en/feature-presence.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Presence / Absence Management
|
||||
|
||||
- [Presence / Absence Management](#presence--absence-management)
|
||||
- [Configure Presence (or Absence)](#configure-presence-or-absence)
|
||||
|
||||
## Configure Presence (or Absence)
|
||||
|
||||
If this feature is selected, it allows you to dynamically adjust the preset temperatures of the thermostat when presence (or absence) is detected. To do this, you need to configure the temperature to be used for each preset when presence is disabled. When the presence sensor turns off, these temperatures will be applied. When it turns back on, the "normal" temperature configured for the preset will be used. See [preset management](feature-presets.md).
|
||||
|
||||
To configure presence, fill out this form:
|
||||
|
||||

|
||||
|
||||
For this, you simply need to configure an **occupancy sensor** whose state must be 'on' or 'home' if someone is present, or 'off' or 'not_home' otherwise.
|
||||
|
||||
Temperatures are configured in the entities of the device corresponding to your _VTherm_ (Settings/Integration/Versatile Thermostat/the vtherm).
|
||||
|
||||
WARNING: People groups do not work as a presence sensor. They are not recognized as a presence sensor. You need to use a template as described here [Using a People Group as a Presence Sensor](troubleshooting.md#using-a-people-group-as-a-presence-sensor).
|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> 1. The temperature change is immediate and is reflected on the front panel. The calculation will consider the new target temperature at the next cycle calculation.
|
||||
> 2. You can use the direct person.xxxx sensor or a Home Assistant sensor group. The presence sensor handles the states `on` or `home` as present and `off` or `not_home` as absent.
|
||||
> 3. To pre-heat your home when everyone is absent, you can add an `input_boolean` entity to your people group. If you set this `input_boolean` to 'On', the presence sensor will be forced to 'On' and the presets with presence will be used. You can also set this `input_boolean` to 'On' via an automation, for example, when you leave a zone to start preheating your home.
|
||||
30
documentation/en/feature-presets.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Presets (Pre-configured Settings)
|
||||
|
||||
- [Presets (Pre-configured Settings)](#presets-pre-configured-settings)
|
||||
- [Configure Pre-configured Temperatures](#configure-pre-configured-temperatures)
|
||||
|
||||
## Configure Pre-configured Temperatures
|
||||
|
||||
The preset mode allows you to pre-configure the target temperature. Used in conjunction with Scheduler (see [scheduler](additions#the-scheduler-component)), you'll have a powerful and simple way to optimize the temperature relative to the electricity consumption in your home. The managed presets are as follows:
|
||||
- **Eco**: the device is in energy-saving mode
|
||||
- **Comfort**: the device is in comfort mode
|
||||
- **Boost**: the device fully opens all valves
|
||||
|
||||
If the AC mode is used, you can also configure temperatures when the equipment is in air conditioning mode.
|
||||
|
||||
**None** is always added to the list of modes, as it is a way to not use presets but instead set a **manual temperature**.
|
||||
|
||||
The presets are configured directly from the _VTherm_ entities or the central configuration if you're using centralized control. Upon creating the _VTherm_, you will have different entities that will allow you to set the temperatures for each preset:
|
||||
|
||||
.
|
||||
|
||||
The list of entities varies depending on your feature choices:
|
||||
1. If the 'presence detection' function is activated, you will have the presets with an "absence" version prefixed with _abs_.
|
||||
2. If you have selected the _AC_ option, you will also have presets for 'air conditioning' prefixed with _clim_.
|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> 1. When you manually change the target temperature, the preset switches to None (no preset).
|
||||
> 2. The standard preset `Away` is a hidden preset that cannot be directly selected. Versatile Thermostat uses presence management or motion detection to automatically and dynamically adjust the target temperature based on presence in the home or activity in the room. See [presence management](feature-presence.md).
|
||||
> 3. If you're using load shedding management, you will see a hidden preset named `power`. The heating element's preset is set to "power" when overload conditions are met and load shedding is active for that heating element. See [power management](feature-power.md).
|
||||
> 4. If you're using advanced configuration, you will see the preset set to `safety` if the temperature could not be retrieved after a certain delay. See [Safety Mode](feature-advanced.md#safety-mode).
|
||||
64
documentation/en/feature-window.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Door/Window Open Detection
|
||||
|
||||
- [Door/Window Open Detection](#doorwindow-open-detection)
|
||||
- [Sensor Mode](#sensor-mode)
|
||||
- [Auto Mode](#auto-mode)
|
||||
|
||||
You must have selected the `With Open Detection` feature on the first page to reach this page.
|
||||
Open detection can be done in two ways:
|
||||
1. By using a sensor placed on the opening (sensor mode),
|
||||
2. By detecting a sudden temperature drop (auto mode)
|
||||
|
||||
## Sensor Mode
|
||||
To switch to sensor mode, you need to provide an entity of type `binary_sensor` or `input_boolean`.
|
||||
In this mode, you need to fill in the following information:
|
||||
|
||||

|
||||
|
||||
1. A **delay in seconds** before any change. This allows you to open a window quickly without stopping the heating.
|
||||
2. The action to take when the opening is detected as open. The possible actions are:
|
||||
1. _Turn off_: the _VTherm_ will be turned off.
|
||||
2. _Fan only_: heating or cooling will be turned off, but the equipment will continue to ventilate (for compatible equipment).
|
||||
3. _Frost protection_: the "Frost Protection" preset temperature will be selected on the _VTherm_ without changing the current preset (see notes below).
|
||||
4. _Eco_: the "Eco" preset temperature will be applied to the _VTherm_ without changing the current preset (see notes below).
|
||||
|
||||
When the detector switches to open:
|
||||
1. _VTherm_ waits for the specified delay.
|
||||
2. If the window is still open after the delay, the _VTherm_ state (Heating / Cooling / ..., current preset, current target temperature) is saved and the action is performed.
|
||||
|
||||
Similarly, when the detector switches to closed:
|
||||
1. _VTherm_ waits for the specified delay.
|
||||
2. If the window is still closed after the delay, the state before the window opening is restored.
|
||||
|
||||
## Auto Mode
|
||||
In auto mode, the configuration is as follows:
|
||||
|
||||

|
||||
|
||||
1. A **delay in seconds** before any change. This allows you to open a window quickly without stopping the heating.
|
||||
2. A detection threshold in degrees per hour. When the temperature drops beyond this threshold, the thermostat will turn off. The lower this value, the faster the detection (with a higher risk of false positives).
|
||||
3. A threshold for ending detection in degrees per hour. When the temperature drop exceeds this value, the thermostat will return to the previous mode (mode and preset).
|
||||
4. A maximum detection duration. Beyond this duration, the thermostat will return to its previous mode and preset even if the temperature continues to drop.
|
||||
5. The action to take when the opening is detected as open. The actions are the same as in sensor mode described above.
|
||||
|
||||
To adjust the thresholds, it is recommended to start with the reference values and adjust the detection thresholds. Some tests gave me the following values (for an office):
|
||||
- Detection threshold: 3°C/hour
|
||||
- No detection threshold: 0°C/hour
|
||||
- Max duration: 30 min.
|
||||
|
||||
A new sensor called "slope" has been added for all thermostats. It provides the slope of the temperature curve in °C/hour (or °K/hour). This slope is smoothed and filtered to avoid aberrant thermometer values that could interfere with the measurement.
|
||||
|
||||

|
||||
|
||||
To adjust it properly, it is recommended to display both the temperature curve and the slope of the curve ("slope") on the same historical graph:
|
||||
|
||||

|
||||
|
||||
>  _*Notes*_
|
||||
>
|
||||
> 1. If you want to use **multiple door/window sensors** to automate your thermostat, simply create a group with the usual behavior (https://www.home-assistant.io/integrations/binary_sensor.group/)
|
||||
> 2. If you don't have a door/window sensor in your room, simply leave the sensor entity ID empty.
|
||||
> 3. **Only one mode is allowed**. You cannot configure a thermostat with both a sensor and auto detection. The two modes might contradict each other, so both modes cannot be active at the same time.
|
||||
> 4. It is not recommended to use auto mode for equipment subjected to frequent and normal temperature variations (hallways, open areas, etc.).
|
||||
> 5. To avoid interfering with your current preset settings, the actions _Frost protection_ and _Eco_ change the target temperature without changing the preset. So, you may notice a discrepancy between the selected preset and the setpoint.
|
||||
> 6. If you use the Versatile Thermostat UI card (see [here](additions.md#even-better-with-the-versatile-thermostat-ui-card)), open detection is represented as follows: .
|
||||
BIN
documentation/en/images/activity-preset-icon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
documentation/en/images/add-an-integration.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
documentation/en/images/auto-start-stop-icon.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
documentation/en/images/central-mode.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
BIN
documentation/en/images/config-advanced.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
documentation/en/images/config-auto-start-stop.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
documentation/en/images/config-central-boiler-1.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
documentation/en/images/config-central-boiler-2.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
documentation/en/images/config-features.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
documentation/en/images/config-linked-entity.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
documentation/en/images/config-linked-entity2.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
documentation/en/images/config-linked-entity3.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
documentation/en/images/config-main.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
documentation/en/images/config-main0.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
documentation/en/images/config-motion.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
documentation/en/images/config-power.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
documentation/en/images/config-presence.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
documentation/en/images/config-preset-temp.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
documentation/en/images/config-self-regulation-valve-1.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
documentation/en/images/config-self-regulation-valve-2.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
documentation/en/images/config-tpi.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
documentation/en/images/config-use-internal-temp.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
documentation/en/images/config-window-auto.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
documentation/en/images/config-window-sensor.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
BIN
documentation/en/images/dev-tools-turnon-boiler-1.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
documentation/en/images/dev-tools-turnon-boiler-2.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
documentation/en/images/enable-auto-start-stop-entity.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
BIN
documentation/en/images/entity-follow-under-temp-change.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
documentation/en/images/motion-detection-icon.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
documentation/en/images/multi-switch-activation.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
documentation/en/images/my-tuning.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
BIN
documentation/en/images/over-climate-schema.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
documentation/en/images/over-switch-schema.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
documentation/en/images/over-valve-schema.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
BIN
documentation/en/images/power-exceeded-icon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
documentation/en/images/results-1.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
BIN
documentation/en/images/results-4.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
BIN
documentation/en/images/results-over-climate-1.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
documentation/en/images/results-over-climate-2.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
documentation/en/images/safety-mode-icon.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
BIN
documentation/en/images/thermostat-entities.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
documentation/en/images/tips.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
documentation/en/images/use-central-mode.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
BIN
documentation/en/images/window-detection-icon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
19
documentation/en/installation.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# How to Install Versatile Thermostat?
|
||||
|
||||
## HACS Installation (Recommended)
|
||||
|
||||
1. Install [HACS](https://hacs.xyz/). This way, you will automatically receive updates.
|
||||
2. The Versatile Thermostat integration is now available directly from the HACS interface (Integrations tab).
|
||||
3. Search for and install "Versatile Thermostat" in HACS and click "Install".
|
||||
4. Restart Home Assistant.
|
||||
5. Then, you can add a Versatile Thermostat integration in the Settings / Integrations page. Add as many thermostats as needed (usually one per radiator or group of radiators that need to be controlled, or per pump in the case of a centralized heating system).
|
||||
|
||||
## Manual Installation
|
||||
|
||||
1. Using your tool of choice, open your Home Assistant configuration directory (where you will find `configuration.yaml`).
|
||||
2. If you don't have a `custom_components` directory, you need to create one.
|
||||
3. Inside the `custom_components` directory, create a new folder called `versatile_thermostat`.
|
||||
4. Download _all_ files from the `custom_components/versatile_thermostat/` directory (folder) in this repository.
|
||||
5. Place the downloaded files in the new folder you created.
|
||||
6. Restart Home Assistant.
|
||||
7. Configure the new Versatile Thermostat integration.
|
||||
1676
documentation/en/one-page.md
Normal file
101
documentation/en/over-climate.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# `over_climate` Type Thermostat
|
||||
|
||||
- [`over_climate` Type Thermostat](#over_climate-type-thermostat)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Configuration](#configuration)
|
||||
- [The Underlying Entities](#the-underlying-entities)
|
||||
- [AC Mode](#ac-mode)
|
||||
- [Self-Regulation](#self-regulation)
|
||||
- [Auto-Fan (Auto Ventilation)](#auto-fan-auto-ventilation)
|
||||
- [Compensating for the Internal Temperature of the Underlying Equipment](#compensating-for-the-internal-temperature-of-the-underlying-equipment)
|
||||
- [Specific Functions](#specific-functions)
|
||||
- [Follow Underlying Temperature Changes](#follow-underlying-temperature-changes)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The installation should look like this:
|
||||
|
||||

|
||||
|
||||
1. The user or automation, or the Scheduler, sets a setpoint via a preset or directly using a temperature.
|
||||
2. Periodically, the internal thermometer (2), external thermometer (2b), or equipment's internal thermometer (2c) sends the measured temperature. The internal thermometer should be placed in a relevant spot for the user's comfort: ideally in the middle of the living space. Avoid placing it too close to a window or equipment.
|
||||
3. Based on the setpoint values, differences, and self-regulation parameters (see [auto-regulation](self-regulation.md)), VTherm will calculate a setpoint to send to the underlying `climate` entity.
|
||||
4. The `climate` entity controls the equipment using its own protocol.
|
||||
5. Depending on the chosen regulation options, VTherm may directly control the opening of a thermostatic valve or calibrate the equipment so that its internal temperature reflects the room temperature.
|
||||
|
||||
## Configuration
|
||||
|
||||
Click on the "Underlying Entities" option from the menu, and you will see this configuration page:
|
||||
|
||||

|
||||
|
||||
### The Underlying Entities
|
||||
In the "Equipment to Control" list, you should add the `climate` entities that will be controlled by VTherm. Only entities of type `climate` are accepted.
|
||||
|
||||
### AC Mode
|
||||
|
||||
You can choose an `over_climate` thermostat to control an air conditioner (reversible or not) by checking the "AC Mode" box. If the equipment allows it, both 'Heating' and 'Cooling' modes will be available.
|
||||
|
||||
### Self-Regulation
|
||||
|
||||
In `over_climate` mode, the device uses its own regulation algorithm: it turns on/off and pauses automatically based on the setpoint transmitted by VTherm through its `climate` entity. It uses its internal thermometer and the received setpoint.
|
||||
|
||||
Depending on the equipment, this internal regulation may vary in quality. It greatly depends on the quality of the equipment, the functionality of its internal thermometer, and its internal algorithm. To improve equipment that regulates poorly, VTherm offers a way to adjust the setpoint it sends by increasing or decreasing it based on the room temperature measured by VTherm, rather than the internal temperature.
|
||||
|
||||
The self-regulation options are described in detail [here](self-regulation.md).
|
||||
|
||||
To avoid overloading the underlying equipment (some may beep unpleasantly, others run on batteries, etc.), two thresholds are available to limit the number of requests:
|
||||
1. Regulation Threshold: a threshold in ° below which a new setpoint will not be sent. If the last setpoint was 22°, the next one will be 22° ± regulation threshold.
|
||||
2. Minimum Regulation Period (in minutes): a minimum time interval below which a new setpoint will not be sent. If the last setpoint was sent at 11:00, the next one cannot be sent before 11:00 + minimum regulation period.
|
||||
|
||||
Improperly setting these thresholds may prevent correct self-regulation as new setpoints won't be sent.
|
||||
|
||||
### Auto-Fan (Auto Ventilation)
|
||||
|
||||
This mode, introduced in version 4.3, forces the use of ventilation if the temperature difference is significant. By activating ventilation, heat distribution occurs more quickly, which helps achieve the target temperature faster.
|
||||
You can choose which ventilation level to activate from the following options: Low, Medium, High, Turbo.
|
||||
|
||||
Of course, your underlying equipment must have ventilation, and it must be controllable for this to work. If your equipment doesn't include the Turbo mode, the High mode will be used instead. Once the temperature difference becomes small again, the ventilation will switch to a "normal" mode, which depends on your equipment (in order): `Mute`, `Auto`, `Low`. The first available mode for your equipment will be chosen.
|
||||
|
||||
### Compensating for the Internal Temperature of the Underlying Equipment
|
||||
|
||||
Sometimes, the internal thermometer of the underlying equipment (TRV, air conditioner, etc.) is inaccurate to the point that self-regulation is insufficient. This happens when the internal thermometer is placed too close to the heat source. The internal temperature rises much faster than the room temperature, leading to regulation failures.
|
||||
Example:
|
||||
1. Room temperature is 18°, setpoint is 20°.
|
||||
2. The internal temperature of the equipment is 22°.
|
||||
3. If VTherm sends a setpoint of 21° (= 20° + 1° of self-regulation), the equipment will not heat because its internal temperature (22°) is higher than the setpoint (21°).
|
||||
|
||||
To address this, a new optional feature has been added in version 5.4: 
|
||||
|
||||
When activated, this feature adds the difference between the internal temperature and the room temperature to the setpoint to force heating.
|
||||
In the above example, the difference is +4° (22° - 18°), so VTherm will send 25° (21° + 4°) to the equipment, forcing it to heat.
|
||||
|
||||
This difference is calculated for each underlying equipment since each has its own internal temperature. For example, a VTherm connected to three TRVs, each with its own internal temperature.
|
||||
|
||||
This results in much more effective self-regulation that avoids issues with large internal temperature differences due to faulty sensors.
|
||||
|
||||
However, be aware that some internal temperatures fluctuate so quickly and inaccurately that they completely skew the calculation. In this case, it’s better to disable this option.
|
||||
|
||||
You will find advice on how to adjust these settings properly on the page [self-regulation](self-regulation.md).
|
||||
|
||||
## Specific Functions
|
||||
|
||||
Specific functions can be configured through a dedicated option in the menu.
|
||||
|
||||
The specific functions that require configuration for this type of VTherm are:
|
||||
1. Auto-Start/Stop: Automatic start and stop of VTherm based on usage forecasts. This is described here: [auto-start/stop function](feature-auto-start-stop.md).
|
||||
2. If valve regulation is chosen, the TPI algorithm configuration is accessible from the menu. See ([algorithms](algorithms.md)).
|
||||
|
||||
## Follow Underlying Temperature Changes
|
||||
|
||||
Some users want to continue using their equipment as before (without _VTherm_). For example, you might want to use the remote control of your _PAC_ or turn the knob on your _TRV_.
|
||||
If you are in this case, an entity has been added to the _VTherm_ device called `Follow underlying temp changes`:
|
||||
|
||||

|
||||
|
||||
When this entity is 'On', all temperature or state changes made directly on the underlying equipment are reflected in _VTherm_.
|
||||
|
||||
Be careful, if you use this feature, your equipment is now controlled in two ways: _VTherm_ and directly by you. The commands might be contradictory, which could lead to confusion about the equipment's state. _VTherm_ is equipped with a delay mechanism that prevents loops: the user gives a setpoint, which is captured by _VTherm_ and changes the setpoint, ... This delay may cause the change made directly on the equipment to be ignored if these changes are too close together in time.
|
||||
|
||||
Some equipment (like Daikin, for example) changes state by itself. If the checkbox is checked, it may turn off the _VTherm_ when that's not what you intended.
|
||||
That's why it's better not to use it. It generates a lot of confusion and many support requests.
|
||||
55
documentation/en/over-switch.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# `over_switch` Type Thermostat
|
||||
|
||||
- [`over_switch` Type Thermostat](#over_switch-type-thermostat)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Configuration](#configuration)
|
||||
- [The Underlying Entities](#the-underlying-entities)
|
||||
- [Keep-Alive](#keep-alive)
|
||||
- [AC Mode](#ac-mode)
|
||||
- [Command Inversion](#command-inversion)
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The installation should look like this:
|
||||
|
||||

|
||||
|
||||
1. The user or automation, or the Scheduler, sets a setpoint via a preset or directly using a temperature.
|
||||
2. Periodically, the internal thermometer (2) or external thermometer (2b) sends the measured temperature. The internal thermometer should be placed in a relevant spot for the user's comfort: ideally in the middle of the living space. Avoid placing it too close to a window or too near the radiator.
|
||||
3. Based on the setpoint values, the different temperatures, and the TPI algorithm parameters (see [TPI](algorithms.md#lalgorithme-tpi)), VTherm will calculate a percentage of the on-time.
|
||||
4. It will then regularly command the turning on and off of the underlying `switch` entities.
|
||||
5. These underlying switch entities will control the physical switch.
|
||||
6. The physical switch will turn the radiator on or off.
|
||||
|
||||
> The on-time percentage is recalculated each cycle, which is what allows regulating the room temperature.
|
||||
|
||||
## Configuration
|
||||
|
||||
Click on the "Underlying Entities" option from the menu, and you will see this configuration page:
|
||||
|
||||

|
||||
|
||||
### The Underlying Entities
|
||||
In the "Equipment to Control" list, you should add the switches that will be controlled by VTherm. Only `switch` or `input_boolean` entities are accepted.
|
||||
|
||||
The algorithm currently available is TPI. See [algorithm](#algorithm).
|
||||
If multiple entities are configured, the thermostat staggers the activations to minimize the number of switches on at any given time. This allows for better power distribution, as each radiator will turn on in turn.
|
||||
|
||||
VTherm will smooth the consumed power as much as possible by alternating activations. Example of staggered activations:
|
||||
|
||||

|
||||
|
||||
Of course, if the requested power (`on_percent`) is too high, there will be an overlap of activations.
|
||||
|
||||
### Keep-Alive
|
||||
|
||||
Some equipment requires periodic activation to prevent a safety shutdown. Known as "keep-alive," this function can be activated by entering a non-zero number of seconds in the thermostat's keep-alive interval field. To disable the function or if in doubt, leave it empty or enter zero (default value).
|
||||
|
||||
### AC Mode
|
||||
|
||||
It is possible to choose a `thermostat_over_switch` to control an air conditioner by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
|
||||
|
||||
### Command Inversion
|
||||
|
||||
If your equipment is controlled by a pilot wire with a diode, you may need to check the "Invert the Command" box. This will set the switch to `On` when you need to turn off the equipment and to `Off` when you need to turn it on. The cycle times will be inverted with this option.
|
||||
30
documentation/en/over-valve.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# `thermostat_over_valve` Type Thermostat
|
||||
|
||||
>  _*Notes*_
|
||||
> 1. The `over_valve` type is often confused with the `over_climate` type equipped with auto-regulation and direct valve control.
|
||||
> 2. You should only choose this type when you do not have an associated `climate` entity for your _TRV_ in Home Assistant, and if you only have a `number` type entity to control the valve's opening percentage. The `over_climate` with auto-regulation on the valve is much more powerful than the `over_valve` type.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The installation should be similar to the `over_switch` VTherm setup, except that the controlled equipment is directly the valve of a _TRV_:
|
||||
|
||||

|
||||
|
||||
1. The user or automation, or the Scheduler, sets a setpoint via a preset or directly using a temperature.
|
||||
2. Periodically, the internal thermometer (2) or external thermometer (2b) or internal thermometer of the equipment (2c) sends the measured temperature. The internal thermometer should be placed in a relevant spot for the user's comfort: ideally in the middle of the living space. Avoid placing it too close to a window or too near the equipment.
|
||||
3. Based on the setpoint values, the different temperatures, and the TPI algorithm parameters (see [TPI](algorithms.md#lalgorithme-tpi)), VTherm will calculate the valve's opening percentage.
|
||||
4. It will then modify the value of the underlying `number` entities.
|
||||
5. These underlying `number` entities will control the valve opening rate on the _TRV_.
|
||||
6. This will regulate the radiator's heating.
|
||||
|
||||
> The opening rate is recalculated each cycle, which allows regulating the room temperature.
|
||||
|
||||
## Configuration
|
||||
|
||||
Click on the "Underlying Entities" option from the menu, and you will see this configuration page, you should add the `number` entities that will be controlled by VTherm. Only `number` or `input_number` entities are accepted.
|
||||
|
||||

|
||||
|
||||
The algorithm currently available is TPI. See [algorithm](#algorithm).
|
||||
|
||||
It is possible to choose a `thermostat_over_valve` to control an air conditioner by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
|
||||
43
documentation/en/presentation.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# When to Use and Not Use It
|
||||
This thermostat can control 3 types of equipment:
|
||||
1. A radiator that only works in on/off mode (called `thermostat_over_switch`). The minimum configuration required to use this type of thermostat is:
|
||||
1. An equipment like a radiator (a `switch` or equivalent),
|
||||
2. A temperature sensor for the room (or an input_number),
|
||||
3. An external temperature sensor (consider the weather integration if you don't have one).
|
||||
2. Another thermostat that has its own modes of operation (called `thermostat_over_climate`). For this type of thermostat, the minimum configuration requires:
|
||||
1. Equipment – like air conditioning, a thermostatic valve – controlled by its own `climate` entity.
|
||||
3. Equipment that can take a value from 0 to 100% (called `thermostat_over_valve`). At 0, heating is off, and at 100% it is fully open. This type allows controlling a thermostatic valve (e.g., Shelly valve) that exposes a `number` type entity, enabling direct control of the valve's opening. Versatile Thermostat regulates the room temperature by adjusting the opening percentage, using both the internal and external temperature sensors, and utilizing the TPI algorithm described below.
|
||||
|
||||
The `over_climate` type allows you to add all the features offered by VersatileThermostat to your existing equipment. The VersatileThermostat `climate` entity will control your underlying `climate` entity, turn it off if windows are open, switch to Eco mode if no one is present, etc. See [here](#pourquoi-un-nouveau-thermostat-implémentation). For this type of thermostat, all heating cycles are controlled by the underlying `climate` entity and not by the Versatile Thermostat itself. An optional auto-regulation function allows Versatile Thermostat to adjust the setpoint temperature to the underlying entity in order to reach the setpoint.
|
||||
|
||||
Installations with a pilot wire and activation diode benefit from an option that allows inverting the on/off control of the underlying radiator. To do this, use the `over_switch` type and check the "Invert command" option.
|
||||
|
||||
# Why a New Thermostat Implementation?
|
||||
|
||||
This component, called __Versatile Thermostat__, manages the following use cases:
|
||||
- Configuration via the standard integration graphical interface (using the Config Entry flow),
|
||||
- Full use of **preset mode**,
|
||||
- Disable preset mode when the temperature is **set manually** on a thermostat,
|
||||
- Turn off/on a thermostat or change preset when a **door or windows are opened/closed** after a certain delay,
|
||||
- Change preset when **activity is detected** or not in a room for a defined time,
|
||||
- Use a **TPI (Time Proportional Interval)** algorithm thanks to [[Argonaute](https://forum.hacf.fr/u/argonaute/summary)],
|
||||
- Add **load shedding** management or regulation to not exceed a defined total power. When the maximum power is exceeded, a hidden preset of "power" is set on the `climate` entity. When the power goes below the maximum, the previous preset is restored.
|
||||
- **Presence management**. This feature allows dynamically modifying the preset temperature based on the presence sensor in your home.
|
||||
- **Actions to interact with the thermostat** from other integrations: you can force presence/non-presence using a service, and you can dynamically change preset temperatures and modify security settings.
|
||||
- Add sensors to view the thermostat's internal states,
|
||||
- Centralized control of all Versatile Thermostats to stop them all, set them all to frost protection, force them all to heating mode (in winter), force them all to cooling mode (in summer).
|
||||
- Control of a central heating boiler and VTherms that must control this boiler.
|
||||
- Automatic start/stop based on usage prediction for `over_climate`.
|
||||
|
||||
All these functions are configurable either centrally or individually depending on your needs.
|
||||
|
||||
## Incompatibilities
|
||||
Some TRV type thermostats are known to be incompatible with Versatile Thermostat. This includes the following valves:
|
||||
1. Danfoss POPP valves with temperature feedback. It is impossible to turn off this valve as it auto-regulates itself, causing conflicts with VTherm.
|
||||
2. "Homematic" thermostats (and possibly Homematic IP) are known to have issues with Versatile Thermostat due to the limitations of the underlying RF protocol. This problem particularly arises when trying to control multiple Homematic thermostats at once in a single VTherm instance. To reduce service cycle load, you can, for example, group thermostats using Homematic-specific procedures (e.g., using a wall-mounted thermostat) and let Versatile Thermostat control only the wall-mounted thermostat directly. Another option is to control a single thermostat and propagate mode and temperature changes via automation.
|
||||
3. Heatzy type thermostats that do not support `set_temperature` commands.
|
||||
4. Rointe type thermostats tend to wake up on their own. The rest works normally.
|
||||
5. TRVs like Aqara SRTS-A01 and MOES TV01-ZB, which lack the `hvac_action` state feedback to determine whether they are heating or not. Therefore, state feedback is inaccurate, but the rest seems functional.
|
||||
6. Airwell air conditioners with the "Midea AC LAN" integration. If two VTherm commands are too close together, the air conditioner stops itself.
|
||||
7. Climates based on the Overkiz integration do not work. It seems impossible to turn off or even change the temperature on these systems.
|
||||
|
||||