Compare commits
44 Commits
6.4.0.beta
...
6.8.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53160f1365 | ||
|
|
1aaa6d4461 | ||
|
|
a09af6a184 | ||
|
|
e9dcb21093 | ||
|
|
980c24c939 | ||
|
|
56cdbf23bc | ||
|
|
ac406ff1f4 | ||
|
|
146f5dfcab | ||
|
|
7103af3634 | ||
|
|
46da1d6fbe | ||
|
|
3529607948 | ||
|
|
0a658b7a2a | ||
|
|
289ccc7bb7 | ||
|
|
c1d1e8f1db | ||
|
|
71c35ecdc0 | ||
|
|
4f8e45dda6 | ||
|
|
d624c327b6 | ||
|
|
b46a24f834 | ||
|
|
d31376d55d | ||
|
|
dbfd294ff3 | ||
|
|
e111bd0647 | ||
|
|
ba69319198 | ||
|
|
f9df925181 | ||
|
|
2d72efe447 | ||
|
|
95af6eba97 | ||
|
|
06dc537767 | ||
|
|
2d79d961dc | ||
|
|
027bf8386b | ||
|
|
a0e548ef71 | ||
|
|
132519b471 | ||
|
|
e6c330fc9d | ||
|
|
968e8286ea | ||
|
|
0f60c070ab | ||
|
|
810430f7b1 | ||
|
|
b4860c2b8d | ||
|
|
60bd522a97 | ||
|
|
fc39cf5f40 | ||
|
|
f6fb7487d5 | ||
|
|
0f585be0c9 | ||
|
|
492c95aff5 | ||
|
|
a530051bbd | ||
|
|
4ef82af8ce | ||
|
|
2ea5cf471b | ||
|
|
f29b2f9b81 |
@@ -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
|
||||
@@ -152,6 +200,7 @@ climate:
|
||||
name: Underlying thermostat2
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
target_sensor: input_number.fake_temperature_sensor1
|
||||
ac_mode: false
|
||||
- platform: generic_thermostat
|
||||
name: Underlying thermostat3
|
||||
heater: input_boolean.fake_heater_switch3
|
||||
@@ -184,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:
|
||||
@@ -237,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:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/issue.md
vendored
@@ -4,6 +4,8 @@ about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
> Please read carefuly this instructions and fill this form before writing an issue. It helps me to help you.
|
||||
|
||||
<!-- This template will allow the maintainer to be efficient and post the more accurante response as possible. There is many types / modes / configuration possible, so the analysis can be very tricky. If don't follow this template, your issue could be rejected without any message. Please help me to help you. -->
|
||||
|
||||
<!-- Before you open a new issue, search through the existing issues to see if others have had the same problem.
|
||||
|
||||
1718
README-fr.md
@@ -38,6 +38,23 @@ from .const import (
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
CONF_UNDERLYING_LIST,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_VALVE,
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
CONF_MAX_ON_PERCENT,
|
||||
)
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
@@ -70,6 +87,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
CONF_AUTO_REGULATION_EXPERT: vol.Schema(SELF_REGULATION_PARAM_SCHEMA),
|
||||
CONF_SHORT_EMA_PARAMS: vol.Schema(EMA_PARAM_SCHEMA),
|
||||
CONF_SAFETY_MODE: vol.Schema(SAFETY_MODE_PARAM_SCHEMA),
|
||||
vol.Optional(CONF_MAX_ON_PERCENT): vol.Coerce(float),
|
||||
}
|
||||
),
|
||||
},
|
||||
@@ -162,13 +180,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
if hass.state == CoreState.running:
|
||||
await api.reload_central_boiler_entities_list()
|
||||
await api.init_vtherm_links()
|
||||
await api.init_vtherm_links(entry.entry_id)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Calling update_listener entry: entry_id='%s', value='%s'",
|
||||
entry.entry_id,
|
||||
entry.data,
|
||||
)
|
||||
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
await reload_all_vtherm(hass)
|
||||
else:
|
||||
@@ -177,7 +202,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
if api is not None:
|
||||
await api.reload_central_boiler_entities_list()
|
||||
await api.init_vtherm_links()
|
||||
await api.init_vtherm_links(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -208,10 +233,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
)
|
||||
new = {**config_entry.data}
|
||||
|
||||
if (
|
||||
config_entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
== CONF_THERMOSTAT_CENTRAL_CONFIG
|
||||
):
|
||||
thermostat_type = config_entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
|
||||
if thermostat_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
new[CONF_USE_WINDOW_FEATURE] = True
|
||||
new[CONF_USE_MOTION_FEATURE] = True
|
||||
new[CONF_USE_POWER_FEATURE] = new.get(CONF_POWER_SENSOR, None) is not None
|
||||
@@ -223,6 +247,50 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"add_central_boiler_control", False
|
||||
) or new.get(CONF_USE_CENTRAL_BOILER_FEATURE, False)
|
||||
|
||||
if config_entry.data.get(CONF_UNDERLYING_LIST, None) is None:
|
||||
underlying_list = []
|
||||
if thermostat_type == CONF_THERMOSTAT_SWITCH:
|
||||
underlying_list = [
|
||||
config_entry.data.get(CONF_HEATER, None),
|
||||
config_entry.data.get(CONF_HEATER_2, None),
|
||||
config_entry.data.get(CONF_HEATER_3, None),
|
||||
config_entry.data.get(CONF_HEATER_4, None),
|
||||
]
|
||||
elif thermostat_type == CONF_THERMOSTAT_CLIMATE:
|
||||
underlying_list = [
|
||||
config_entry.data.get(CONF_CLIMATE, None),
|
||||
config_entry.data.get(CONF_CLIMATE_2, None),
|
||||
config_entry.data.get(CONF_CLIMATE_3, None),
|
||||
config_entry.data.get(CONF_CLIMATE_4, None),
|
||||
]
|
||||
elif thermostat_type == CONF_THERMOSTAT_VALVE:
|
||||
underlying_list = [
|
||||
config_entry.data.get(CONF_VALVE, None),
|
||||
config_entry.data.get(CONF_VALVE_2, None),
|
||||
config_entry.data.get(CONF_VALVE_3, None),
|
||||
config_entry.data.get(CONF_VALVE_4, None),
|
||||
]
|
||||
|
||||
new[CONF_UNDERLYING_LIST] = [
|
||||
entity for entity in underlying_list if entity is not None
|
||||
]
|
||||
|
||||
for key in [
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_VALVE,
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
]:
|
||||
new.pop(key, None)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data=new,
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" This file implements the Auto start/stop algorithm as described here: https://github.com/jmcollin78/versatile_thermostat/issues/585
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .const import (
|
||||
AUTO_START_STOP_LEVEL_NONE,
|
||||
AUTO_START_STOP_LEVEL_FAST,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM,
|
||||
AUTO_START_STOP_LEVEL_SLOW,
|
||||
TYPE_AUTO_START_STOP_LEVELS,
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Some constant to make algorithm depending of level
|
||||
DT_MIN = {
|
||||
AUTO_START_STOP_LEVEL_NONE: 0, # Not used
|
||||
AUTO_START_STOP_LEVEL_SLOW: 30,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM: 15,
|
||||
AUTO_START_STOP_LEVEL_FAST: 7,
|
||||
}
|
||||
|
||||
# the measurement cycle (2 min)
|
||||
CYCLE_SEC = 120
|
||||
|
||||
# A temp hysteresis to avoid rapid OFF/ON
|
||||
TEMP_HYSTERESIS = 0.5
|
||||
|
||||
ERROR_THRESHOLD = {
|
||||
AUTO_START_STOP_LEVEL_NONE: 0, # Not used
|
||||
AUTO_START_STOP_LEVEL_SLOW: 10, # 10 cycle above 1° or 5 cycle above 2°, ...
|
||||
AUTO_START_STOP_LEVEL_MEDIUM: 5, # 5 cycle above 1° or 3 cycle above 2°, ..., 1 cycle above 5°
|
||||
AUTO_START_STOP_LEVEL_FAST: 2, # 2 cycle above 1° or 1 cycle above 2°
|
||||
}
|
||||
|
||||
AUTO_START_STOP_ACTION_OFF = "turnOff"
|
||||
AUTO_START_STOP_ACTION_ON = "turnOn"
|
||||
AUTO_START_STOP_ACTION_NOTHING = "nothing"
|
||||
AUTO_START_STOP_ACTIONS = Literal[ # pylint: disable=invalid-name
|
||||
AUTO_START_STOP_ACTION_OFF,
|
||||
AUTO_START_STOP_ACTION_ON,
|
||||
AUTO_START_STOP_ACTION_NOTHING,
|
||||
]
|
||||
|
||||
class AutoStartStopDetectionAlgorithm:
|
||||
"""The class that implements the algorithm listed above"""
|
||||
|
||||
_dt: float | None = None
|
||||
_level: str = AUTO_START_STOP_LEVEL_NONE
|
||||
_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):
|
||||
"""Initialize a new level"""
|
||||
if level == self._level:
|
||||
return
|
||||
|
||||
self._level = level
|
||||
if self._level != AUTO_START_STOP_LEVEL_NONE:
|
||||
self._dt = DT_MIN[level]
|
||||
self._error_threshold = ERROR_THRESHOLD[level]
|
||||
# reset accumulated error if we change the level
|
||||
self._accumulated_error = 0
|
||||
|
||||
def calculate_action(
|
||||
self,
|
||||
hvac_mode: HVACMode | None,
|
||||
saved_hvac_mode: HVACMode | None,
|
||||
target_temp: float,
|
||||
current_temp: float,
|
||||
slope_min: float | None,
|
||||
now: datetime,
|
||||
) -> AUTO_START_STOP_ACTIONS:
|
||||
"""Calculate an eventual action to do depending of the value in parameter"""
|
||||
if self._level == AUTO_START_STOP_LEVEL_NONE:
|
||||
_LOGGER.debug(
|
||||
"%s - auto-start/stop is disabled",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - calculate_action: hvac_mode=%s, saved_hvac_mode=%s, target_temp=%s, current_temp=%s, slope_min=%s at %s",
|
||||
self,
|
||||
hvac_mode,
|
||||
saved_hvac_mode,
|
||||
target_temp,
|
||||
current_temp,
|
||||
slope_min,
|
||||
now,
|
||||
)
|
||||
|
||||
if hvac_mode is None or target_temp is None or current_temp is None:
|
||||
_LOGGER.debug(
|
||||
"%s - No all mandatory parameters are set. Disable auto-start/stop",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
# Calculate the error factor (P)
|
||||
error = target_temp - current_temp
|
||||
|
||||
# reduce the error considering the dt between the last measurement
|
||||
if self._last_calculation_date is not None:
|
||||
dtmin = (now - self._last_calculation_date).total_seconds() / CYCLE_SEC
|
||||
# ignore two calls too near (< 24 sec)
|
||||
if dtmin <= 0.2:
|
||||
_LOGGER.debug(
|
||||
"%s - new calculation of auto_start_stop (%s) is too near of the last one (%s). Forget it",
|
||||
self,
|
||||
now,
|
||||
self._last_calculation_date,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
error = error * dtmin
|
||||
|
||||
# If the error have change its sign, reset smoothly the accumulated error
|
||||
if error * self._accumulated_error < 0:
|
||||
self._accumulated_error = self._accumulated_error / 2.0
|
||||
|
||||
self._accumulated_error += error
|
||||
|
||||
# Capping of the error
|
||||
self._accumulated_error = min(
|
||||
self._error_threshold,
|
||||
max(-self._error_threshold, self._accumulated_error),
|
||||
)
|
||||
|
||||
self._last_calculation_date = now
|
||||
|
||||
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)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
if hvac_mode == HVACMode.COOL:
|
||||
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(
|
||||
"%s - nothing to do, we are cooling",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
# check to turn on
|
||||
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT:
|
||||
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(
|
||||
"%s - nothing to do, we don't need to heat soon",
|
||||
self,
|
||||
)
|
||||
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
|
||||
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(
|
||||
"%s - nothing to do, we don't need to cool soon",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - nothing to do, no conditions applied",
|
||||
self,
|
||||
)
|
||||
return AUTO_START_STOP_ACTION_NOTHING
|
||||
|
||||
def set_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
|
||||
"""Set a new level"""
|
||||
self._init_level(level)
|
||||
|
||||
@property
|
||||
def dt_min(self) -> float:
|
||||
"""Get the dt value"""
|
||||
return self._dt
|
||||
|
||||
@property
|
||||
def accumulated_error(self) -> float:
|
||||
"""Get the accumulated error value"""
|
||||
return self._accumulated_error
|
||||
|
||||
@property
|
||||
def accumulated_error_threshold(self) -> float:
|
||||
"""Get the accumulated error threshold value"""
|
||||
return self._error_threshold
|
||||
|
||||
@property
|
||||
def level(self) -> TYPE_AUTO_START_STOP_LEVELS:
|
||||
"""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,
|
||||
@@ -19,7 +18,10 @@ from homeassistant.core import (
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.restore_state import (
|
||||
RestoreEntity,
|
||||
async_get as restore_async_get,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
@@ -62,72 +64,7 @@ from homeassistant.const import (
|
||||
STATE_NOT_HOME,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_LAST_SEEN_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_WINDOW_DELAY,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_MOTION_DELAY,
|
||||
CONF_MOTION_OFF_DELAY,
|
||||
CONF_MOTION_PRESET,
|
||||
CONF_NO_MOTION_PRESET,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_PRESETS,
|
||||
# CONF_PRESETS_AWAY,
|
||||
# CONF_PRESETS_WITH_AC,
|
||||
# CONF_PRESETS_AWAY_WITH_AC,
|
||||
CONF_CYCLE_MIN,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_TPI_COEF_INT,
|
||||
CONF_TPI_COEF_EXT,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
CONF_PRESET_POWER,
|
||||
SUPPORT_FLAGS,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_POWER,
|
||||
PRESET_SECURITY,
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
PRESET_AWAY_SUFFIX,
|
||||
CONF_SECURITY_DELAY_MIN,
|
||||
CONF_SECURITY_MIN_ON_PERCENT,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT,
|
||||
DEFAULT_SECURITY_MIN_ON_PERCENT,
|
||||
DEFAULT_SECURITY_DEFAULT_ON_PERCENT,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG,
|
||||
CONF_USE_MOTION_CENTRAL_CONFIG,
|
||||
CONF_USE_POWER_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_FEATURE,
|
||||
CONF_TEMP_MAX,
|
||||
CONF_TEMP_MIN,
|
||||
HIDDEN_PRESETS,
|
||||
CONF_AC_MODE,
|
||||
EventType,
|
||||
ATTR_MEAN_POWER_CYCLE,
|
||||
ATTR_TOTAL_ENERGY,
|
||||
PRESET_AC_SUFFIX,
|
||||
DEFAULT_SHORT_EMA_PARAMS,
|
||||
CENTRAL_MODE_AUTO,
|
||||
CENTRAL_MODE_STOPPED,
|
||||
CENTRAL_MODE_HEAT_ONLY,
|
||||
CENTRAL_MODE_COOL_ONLY,
|
||||
CENTRAL_MODE_FROST_PROTECTION,
|
||||
send_vtherm_event,
|
||||
)
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
@@ -142,13 +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)
|
||||
|
||||
|
||||
class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"""Representation of a base class for all Versatile Thermostat device."""
|
||||
|
||||
@@ -197,8 +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",
|
||||
"have_valve_regulation",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -262,6 +196,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self._attr_translation_key = "versatile_thermostat"
|
||||
|
||||
self._total_energy = None
|
||||
_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
|
||||
@@ -303,6 +238,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
|
||||
self._use_central_config_temperature = False
|
||||
|
||||
self._hvac_off_reason: HVAC_OFF_REASONS | None = None
|
||||
|
||||
self.post_init(entry_infos)
|
||||
|
||||
def clean_central_config_doublon(
|
||||
@@ -517,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
|
||||
@@ -532,6 +469,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self._presence_state = None
|
||||
|
||||
self._total_energy = None
|
||||
_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
|
||||
@@ -560,6 +498,8 @@ 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
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Creation of a new VersatileThermostat entity: unique_id=%s",
|
||||
self,
|
||||
@@ -647,14 +587,24 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
# issue 428. Link to others entities will start at link
|
||||
# await self.async_startup()
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Try to force backup of entity"""
|
||||
_LOGGER.debug(
|
||||
"%s - force write before remove. Energy is %s", self, self.total_energy
|
||||
)
|
||||
# Force dump in background
|
||||
await restore_async_get(self.hass).async_dump_states()
|
||||
|
||||
def remove_thermostat(self):
|
||||
"""Called when the thermostat will be removed"""
|
||||
_LOGGER.info("%s - Removing thermostat", self)
|
||||
|
||||
for under in self._underlyings:
|
||||
under.remove_entity()
|
||||
|
||||
async def async_startup(self, central_configuration):
|
||||
"""Triggered on startup, used to get old state and set internal states accordingly"""
|
||||
"""Triggered on startup, used to get old state and set internal states accordingly. This is triggered by
|
||||
VTherm API"""
|
||||
_LOGGER.debug("%s - Calling async_startup", self)
|
||||
|
||||
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||
@@ -848,18 +798,29 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
else:
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
|
||||
# Restore old hvac_off_reason
|
||||
self._hvac_off_reason = old_state.attributes.get(HVAC_OFF_REASON_NAME, None)
|
||||
|
||||
if old_state.state in [
|
||||
HVACMode.OFF,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.COOL,
|
||||
]:
|
||||
self._hvac_mode = old_state.state
|
||||
else:
|
||||
if not self._hvac_mode:
|
||||
self._hvac_mode = HVACMode.OFF
|
||||
|
||||
# restpre also saved info so that window detection will work
|
||||
self._saved_hvac_mode = old_state.attributes.get("saved_hvac_mode", None)
|
||||
self._saved_preset_mode = old_state.attributes.get(
|
||||
"saved_preset_mode", None
|
||||
)
|
||||
|
||||
old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY)
|
||||
self._total_energy = old_total_energy if old_total_energy else 0
|
||||
self._total_energy = old_total_energy if old_total_energy is not None else 0
|
||||
_LOGGER.debug(
|
||||
"%s - get_my_previous_state restored energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
|
||||
self.restore_specific_previous_state(old_state)
|
||||
else:
|
||||
@@ -873,13 +834,20 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"No previously saved temperature, setting to %s", self._target_temp
|
||||
)
|
||||
self._total_energy = 0
|
||||
_LOGGER.debug(
|
||||
"%s - get_my_previous_state no previous state energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
|
||||
self._saved_target_temp = self._target_temp
|
||||
|
||||
# Set default state to off
|
||||
if not self._hvac_mode:
|
||||
self._hvac_mode = HVACMode.OFF
|
||||
|
||||
if not self.is_on and self.hvac_off_reason is None:
|
||||
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
|
||||
|
||||
self._saved_target_temp = self._target_temp
|
||||
|
||||
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
|
||||
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
|
||||
|
||||
@@ -987,16 +955,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
"""Return current operation."""
|
||||
# Issue #114 - returns my current hvac_mode and not the underlying hvac_mode which could be different
|
||||
# delta will be managed by climate_state_change event.
|
||||
# if self.is_over_climate:
|
||||
# if one not OFF -> return it
|
||||
# else OFF
|
||||
# for under in self._underlyings:
|
||||
# if (mode := under.hvac_mode) not in [HVACMode.OFF]
|
||||
# return mode
|
||||
# return HVACMode.OFF
|
||||
|
||||
return self._hvac_mode
|
||||
|
||||
@property
|
||||
@@ -1038,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."""
|
||||
@@ -1165,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:
|
||||
@@ -1193,6 +1165,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"""True if this VTHerm uses the central configuration temperature"""
|
||||
return self._use_central_config_temperature
|
||||
|
||||
@property
|
||||
def hvac_off_reason(self) -> HVAC_OFF_REASONS:
|
||||
"""Returns the reason of the last switch to HVAC_OFF
|
||||
This is useful for features that turns off the VTherm like
|
||||
window detection or auto-start-stop"""
|
||||
return self._hvac_off_reason
|
||||
|
||||
def underlying_entity_id(self, index=0) -> str | None:
|
||||
"""The climate_entity_id. Added for retrocompatibility reason"""
|
||||
if index < self.nb_underlying_entities:
|
||||
@@ -1234,6 +1213,24 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
if hvac_mode is None:
|
||||
return
|
||||
|
||||
def save_state():
|
||||
self.reset_last_change_time()
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
|
||||
|
||||
# If we already are in OFF, the manual OFF should just overwrite the reason and saved_hvac_mode
|
||||
if self._hvac_mode == HVACMode.OFF and hvac_mode == HVACMode.OFF:
|
||||
_LOGGER.info(
|
||||
"%s - already in OFF. Change the reason to MANUAL and erase the saved_havc_mode"
|
||||
)
|
||||
self._hvac_off_reason = HVAC_OFF_REASON_MANUAL
|
||||
self._saved_hvac_mode = HVACMode.OFF
|
||||
|
||||
save_state()
|
||||
|
||||
return
|
||||
|
||||
self._hvac_mode = hvac_mode
|
||||
|
||||
# Delegate to all underlying
|
||||
@@ -1256,11 +1253,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
# Ensure we update the current operation after changing the mode
|
||||
self.reset_last_temperature_time()
|
||||
|
||||
self.reset_last_change_time()
|
||||
if self._hvac_mode != HVACMode.OFF:
|
||||
self.set_hvac_off_reason(None)
|
||||
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
|
||||
save_state()
|
||||
|
||||
@overrides
|
||||
async def async_set_preset_mode(
|
||||
@@ -1354,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):
|
||||
@@ -1364,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):
|
||||
@@ -1397,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:
|
||||
@@ -1467,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
|
||||
@@ -1654,9 +1653,28 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
|
||||
if not long_enough:
|
||||
_LOGGER.debug(
|
||||
"Motion delay condition is not satisfied. Ignore motion event"
|
||||
"Motion delay condition is not satisfied (the sensor have change its state during the delay). Check motion sensor state"
|
||||
)
|
||||
else:
|
||||
# Get sensor current state
|
||||
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
|
||||
_LOGGER.debug(
|
||||
"%s - motion_state=%s, new_state.state=%s",
|
||||
self,
|
||||
motion_state.state,
|
||||
new_state.state,
|
||||
)
|
||||
if (
|
||||
motion_state.state == new_state.state
|
||||
and new_state.state == STATE_ON
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - the motion sensor is finally 'on' after the delay", self
|
||||
)
|
||||
long_enough = True
|
||||
else:
|
||||
long_enough = False
|
||||
|
||||
if long_enough:
|
||||
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
|
||||
self._motion_state = new_state.state
|
||||
if self._attr_preset_mode == PRESET_ACTIVITY:
|
||||
@@ -1679,6 +1697,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
)
|
||||
self.recalculate()
|
||||
await self.async_control_heating(force=True)
|
||||
else:
|
||||
self._motion_state = (
|
||||
STATE_ON if new_state.state == STATE_OFF else STATE_OFF
|
||||
)
|
||||
|
||||
self._motion_call_cancel = None
|
||||
|
||||
im_on = self._motion_state == STATE_ON
|
||||
@@ -1736,6 +1759,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
for under in self._underlyings:
|
||||
await under.check_initial_state(self._hvac_mode)
|
||||
|
||||
# Prevent from starting a VTherm if window is open
|
||||
if (
|
||||
self.is_window_auto_enabled
|
||||
and self._window_sensor_entity_id is not None
|
||||
and self._hass.states.is_state(self._window_sensor_entity_id, STATE_ON)
|
||||
and self.is_on
|
||||
and self.window_action == CONF_WINDOW_TURN_OFF
|
||||
):
|
||||
_LOGGER.info("%s - the window is open. Prevent starting the VTherm")
|
||||
self._window_auto_state = True
|
||||
self.save_hvac_mode()
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
# Starts the initial control loop (don't wait for an update of temperature)
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
@@ -1881,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)
|
||||
@@ -1971,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(
|
||||
@@ -2072,6 +2113,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self._hvac_mode,
|
||||
)
|
||||
|
||||
def set_hvac_off_reason(self, hvac_off_reason: HVAC_OFF_REASONS):
|
||||
"""Set the reason of hvac_off"""
|
||||
self._hvac_off_reason = hvac_off_reason
|
||||
|
||||
async def restore_hvac_mode(self, need_control_heating=False):
|
||||
"""Restore a previous hvac_mod"""
|
||||
await self.async_set_hvac_mode(self._saved_hvac_mode, need_control_heating)
|
||||
@@ -2203,27 +2248,34 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
if self.window_state is not STATE_ON and not first_init:
|
||||
await self.restore_hvac_mode()
|
||||
await self.restore_preset_mode()
|
||||
|
||||
elif self.window_state is STATE_ON and self.hvac_mode == HVACMode.OFF:
|
||||
# do not restore but mark the reason of off with window detection
|
||||
self.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION)
|
||||
return
|
||||
|
||||
if old_central_mode == CENTRAL_MODE_AUTO and self.window_state is not STATE_ON:
|
||||
save_all()
|
||||
|
||||
if new_central_mode == CENTRAL_MODE_STOPPED:
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
if self.hvac_mode != HVACMode.OFF:
|
||||
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
return
|
||||
|
||||
if new_central_mode == CENTRAL_MODE_COOL_ONLY:
|
||||
if HVACMode.COOL in self.hvac_modes:
|
||||
await self.async_set_hvac_mode(HVACMode.COOL)
|
||||
else:
|
||||
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
return
|
||||
|
||||
if new_central_mode == CENTRAL_MODE_HEAT_ONLY:
|
||||
if HVACMode.HEAT in self.hvac_modes:
|
||||
await self.async_set_hvac_mode(HVACMode.HEAT)
|
||||
else:
|
||||
# if not already off
|
||||
elif self.hvac_mode != HVACMode.OFF:
|
||||
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
return
|
||||
|
||||
@@ -2237,6 +2289,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
PRESET_FROST_PROTECTION, overwrite_saved_preset=False
|
||||
)
|
||||
else:
|
||||
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
return
|
||||
|
||||
@@ -2247,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)
|
||||
@@ -2416,17 +2470,27 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"""Change the window detection state.
|
||||
new_state is on if an open window have been detected or off else
|
||||
"""
|
||||
if not new_state:
|
||||
if new_state is False:
|
||||
_LOGGER.info(
|
||||
"%s - Window is closed. Restoring hvac_mode '%s' if central_mode is not STOPPED",
|
||||
"%s - Window is closed. Restoring hvac_mode '%s' if stopped by window detection or temperature %s",
|
||||
self,
|
||||
self._saved_hvac_mode,
|
||||
self._saved_target_temp,
|
||||
)
|
||||
if self._window_action in [CONF_WINDOW_FROST_TEMP, CONF_WINDOW_ECO_TEMP]:
|
||||
await self._async_internal_set_temperature(self._saved_target_temp)
|
||||
|
||||
# default to TURN_OFF
|
||||
elif self._window_action in [CONF_WINDOW_TURN_OFF, CONF_WINDOW_FAN_ONLY]:
|
||||
elif self._window_action in [CONF_WINDOW_TURN_OFF]:
|
||||
if (
|
||||
self.last_central_mode != CENTRAL_MODE_STOPPED
|
||||
and self.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
|
||||
):
|
||||
self.set_hvac_off_reason(None)
|
||||
await self.restore_hvac_mode(True)
|
||||
elif self._window_action in [CONF_WINDOW_FAN_ONLY]:
|
||||
if self.last_central_mode != CENTRAL_MODE_STOPPED:
|
||||
self.set_hvac_off_reason(None)
|
||||
await self.restore_hvac_mode(True)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
@@ -2436,8 +2500,14 @@ 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(
|
||||
"%s is already off. Forget turning off VTherm due to window detection"
|
||||
)
|
||||
return
|
||||
|
||||
if self.last_central_mode in [CENTRAL_MODE_AUTO, None]:
|
||||
if self._window_action in [CONF_WINDOW_TURN_OFF, CONF_WINDOW_FAN_ONLY]:
|
||||
self.save_hvac_mode()
|
||||
@@ -2467,6 +2537,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self.find_preset_temp(PRESET_ECO)
|
||||
)
|
||||
else: # default is to turn_off
|
||||
self.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION)
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
async def async_control_heating(self, force=False, _=None) -> bool:
|
||||
@@ -2601,16 +2672,40 @@ 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.debug(
|
||||
"%s - update_custom_attributes saved energy is %s",
|
||||
self,
|
||||
self.total_energy,
|
||||
)
|
||||
|
||||
@overrides
|
||||
def async_write_ha_state(self):
|
||||
"""overrides to have log"""
|
||||
_LOGGER.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
|
||||
|
||||
@@ -100,7 +100,7 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
entry_infos,
|
||||
) -> None:
|
||||
"""Initialize the SecurityState Binary sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
super().__init__(hass, unique_id, name)
|
||||
self._attr_name = "Security state"
|
||||
self._attr_unique_id = f"{self._device_name}_security_state"
|
||||
self._attr_is_on = False
|
||||
@@ -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,39 +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"""
|
||||
@@ -109,17 +87,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
|
||||
)
|
||||
self._infos[CONF_USE_MOTION_FEATURE] = self._infos.get(
|
||||
CONF_USE_MOTION_FEATURE
|
||||
CONF_USE_MOTION_FEATURE, False
|
||||
) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
|
||||
|
||||
self._infos[CONF_USE_POWER_FEATURE] = self._infos.get(
|
||||
CONF_USE_POWER_CENTRAL_CONFIG
|
||||
CONF_USE_POWER_CENTRAL_CONFIG, False
|
||||
) or (
|
||||
self._infos.get(CONF_POWER_SENSOR) is not None
|
||||
and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
|
||||
)
|
||||
self._infos[CONF_USE_PRESENCE_FEATURE] = (
|
||||
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG)
|
||||
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False)
|
||||
or self._infos.get(CONF_PRESENCE_SENSOR) is not None
|
||||
)
|
||||
|
||||
@@ -128,6 +106,11 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
and self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV) is not None
|
||||
)
|
||||
|
||||
self._infos[CONF_USE_AUTO_START_STOP_FEATURE] = (
|
||||
self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False) is True
|
||||
and self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
|
||||
)
|
||||
|
||||
def _init_central_config_flags(self, infos):
|
||||
"""Initialisation of central configuration flags"""
|
||||
is_empty: bool = not bool(infos)
|
||||
@@ -140,19 +123,62 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||
CONF_USE_CENTRAL_MODE,
|
||||
):
|
||||
if not is_empty:
|
||||
current_config = self._infos.get(config, None)
|
||||
self._infos[config] = current_config is True or (
|
||||
current_config is None and self._central_config is not None
|
||||
|
||||
self._infos[config] = self._central_config is not None and (
|
||||
current_config is True or current_config is None
|
||||
)
|
||||
# self._infos[config] = current_config is True or (
|
||||
# current_config is None and self._central_config is not None
|
||||
# )
|
||||
else:
|
||||
self._infos[config] = self._central_config is not None
|
||||
|
||||
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.
|
||||
@@ -160,7 +186,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
|
||||
# check the heater_entity_id
|
||||
for conf in [
|
||||
CONF_HEATER,
|
||||
CONF_UNDERLYING_LIST,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
@@ -168,15 +194,20 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
CONF_CLIMATE,
|
||||
CONF_OFFSET_CALIBRATION_LIST,
|
||||
CONF_OPENING_DEGREE_LIST,
|
||||
CONF_CLOSING_DEGREE_LIST,
|
||||
]:
|
||||
d = data.get(conf, None) # pylint: disable=invalid-name
|
||||
if d is not None and self.hass.states.get(d) is None:
|
||||
_LOGGER.error(
|
||||
"Entity id %s doesn't have any state. We cannot use it in the Versatile Thermostat configuration", # pylint: disable=line-too-long
|
||||
d,
|
||||
)
|
||||
raise UnknownEntity(conf)
|
||||
if not isinstance(d, list):
|
||||
d = [d]
|
||||
for e in d:
|
||||
if e is not None and self.hass.states.get(e) is None:
|
||||
_LOGGER.error(
|
||||
"Entity id %s doesn't have any state. We cannot use it in the Versatile Thermostat configuration", # pylint: disable=line-too-long
|
||||
e,
|
||||
)
|
||||
raise UnknownEntity(conf)
|
||||
|
||||
# Check that only one window feature is used
|
||||
ws = self._infos.get(CONF_WINDOW_SENSOR) # pylint: disable=invalid-name
|
||||
@@ -202,6 +233,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||
CONF_USE_CENTRAL_MODE,
|
||||
# CONF_USE_CENTRAL_BOILER_FEATURE, this is for Central Config
|
||||
CONF_USED_BY_CENTRAL_BOILER,
|
||||
]:
|
||||
if data.get(conf) is True:
|
||||
_LOGGER.error(
|
||||
@@ -220,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 = (
|
||||
@@ -265,21 +304,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_SWITCH
|
||||
and infos.get(CONF_HEATER, None) is None
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
|
||||
and infos.get(CONF_CLIMATE, None) is None
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE
|
||||
and infos.get(CONF_VALVE, None) is None
|
||||
if infos.get(CONF_UNDERLYING_LIST, None) is not None and not infos.get(
|
||||
CONF_UNDERLYING_LIST, None
|
||||
):
|
||||
return False
|
||||
|
||||
@@ -312,6 +338,25 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
infos.get(CONF_PROP_FUNCTION, None) == PROPORTIONAL_FUNCTION_TPI
|
||||
and infos.get(CONF_USE_TPI_CENTRAL_CONFIG, False) is False
|
||||
and (
|
||||
infos.get(CONF_TPI_COEF_INT, None) is None
|
||||
or infos.get(CONF_TPI_COEF_EXT) is None
|
||||
)
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
infos.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False) is True
|
||||
and self._central_config is None
|
||||
):
|
||||
return False
|
||||
|
||||
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):
|
||||
@@ -341,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:
|
||||
@@ -352,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"
|
||||
@@ -403,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")
|
||||
|
||||
@@ -431,6 +479,16 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
if self._infos[CONF_USE_PRESENCE_FEATURE] is True:
|
||||
menu_options.append("presence")
|
||||
|
||||
if self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE) is True and self._infos[
|
||||
CONF_THERMOSTAT_TYPE
|
||||
] in [
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
]:
|
||||
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):
|
||||
@@ -500,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
|
||||
@@ -520,17 +595,45 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"""Handle the Type flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_features user_input=%s", user_input)
|
||||
|
||||
schema = STEP_FEATURES_DATA_SCHEMA
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
schema = STEP_CENTRAL_FEATURES_DATA_SCHEMA
|
||||
elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CLIMATE:
|
||||
schema = STEP_CLIMATE_FEATURES_DATA_SCHEMA
|
||||
|
||||
return await self.generic_step(
|
||||
"features",
|
||||
(
|
||||
STEP_CENTRAL_FEATURES_DATA_SCHEMA
|
||||
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG
|
||||
else STEP_FEATURES_DATA_SCHEMA
|
||||
),
|
||||
schema,
|
||||
user_input,
|
||||
self.async_step_menu,
|
||||
)
|
||||
|
||||
async def async_step_auto_start_stop(self, user_input: dict | None = None) -> FlowResult:
|
||||
""" Handle the Auto start stop step"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_auto_start_stop user_input=%s", user_input)
|
||||
|
||||
schema = STEP_AUTO_START_STOP
|
||||
self._infos[COMES_FROM] = None
|
||||
next_step = self.async_step_menu
|
||||
|
||||
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)
|
||||
@@ -869,6 +972,8 @@ class VersatileThermostatOptionsFlowHandler(
|
||||
if not self._infos[CONF_USE_CENTRAL_BOILER_FEATURE]:
|
||||
self._infos[CONF_CENTRAL_BOILER_ACTIVATION_SRV] = None
|
||||
self._infos[CONF_CENTRAL_BOILER_DEACTIVATION_SRV] = None
|
||||
if not self._infos[CONF_USE_AUTO_START_STOP_FEATURE]:
|
||||
self._infos[CONF_AUTO_START_STOP_LEVEL] = AUTO_START_STOP_LEVEL_NONE
|
||||
|
||||
_LOGGER.info(
|
||||
"Recreating entry %s due to configuration change. New config is now: %s",
|
||||
|
||||
@@ -68,6 +68,16 @@ STEP_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CLIMATE_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USE_AUTO_START_STOP_FEATURE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_CENTRAL_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
|
||||
@@ -109,17 +119,10 @@ STEP_CENTRAL_BOILER_SCHEMA = vol.Schema(
|
||||
|
||||
STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_HEATER): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_2): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_3): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_4): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]),
|
||||
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_KEEP_ALIVE): cv.positive_int,
|
||||
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
|
||||
@@ -134,17 +137,8 @@ STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
|
||||
|
||||
STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_CLIMATE): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||
),
|
||||
vol.Optional(CONF_CLIMATE_2): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||
),
|
||||
vol.Optional(CONF_CLIMATE_3): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||
),
|
||||
vol.Optional(CONF_CLIMATE_4): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN, multiple=True),
|
||||
),
|
||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||
vol.Optional(
|
||||
@@ -173,17 +167,10 @@ STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
|
||||
|
||||
STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_VALVE): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_VALVE_2): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_VALVE_3): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
),
|
||||
vol.Optional(CONF_VALVE_4): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN]),
|
||||
vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In(
|
||||
[
|
||||
@@ -196,6 +183,45 @@ STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
|
||||
}
|
||||
)
|
||||
|
||||
STEP_AUTO_START_STOP = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_AUTO_START_STOP_LEVEL, default=AUTO_START_STOP_LEVEL_NONE
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_AUTO_START_STOP_LEVELS,
|
||||
translation_key="auto_start_stop",
|
||||
mode="dropdown",
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
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,
|
||||
@@ -204,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,8 +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 (
|
||||
@@ -15,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,
|
||||
@@ -22,8 +27,8 @@ from .prop_algorithm import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_VERSION = 1
|
||||
CONFIG_MINOR_VERSION = 2
|
||||
CONFIG_VERSION = 2
|
||||
CONFIG_MINOR_VERSION = 0
|
||||
|
||||
PRESET_TEMP_SUFFIX = "_temp"
|
||||
PRESET_AC_SUFFIX = "_ac"
|
||||
@@ -51,12 +56,10 @@ PLATFORMS: list[Platform] = [
|
||||
# Number should be after CLIMATE
|
||||
Platform.NUMBER,
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
CONF_HEATER_2 = "heater_entity2_id"
|
||||
CONF_HEATER_3 = "heater_entity3_id"
|
||||
CONF_HEATER_4 = "heater_entity4_id"
|
||||
CONF_UNDERLYING_LIST = "underlying_entity_ids"
|
||||
CONF_HEATER_KEEP_ALIVE = "heater_keep_alive"
|
||||
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
|
||||
CONF_LAST_SEEN_TEMP_SENSOR = "last_seen_temperature_sensor_entity_id"
|
||||
@@ -88,25 +91,19 @@ CONF_THERMOSTAT_CENTRAL_CONFIG = "thermostat_central_config"
|
||||
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
|
||||
CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate"
|
||||
CONF_THERMOSTAT_VALVE = "thermostat_over_valve"
|
||||
CONF_CLIMATE = "climate_entity_id"
|
||||
CONF_CLIMATE_2 = "climate_entity2_id"
|
||||
CONF_CLIMATE_3 = "climate_entity3_id"
|
||||
CONF_CLIMATE_4 = "climate_entity4_id"
|
||||
CONF_USE_WINDOW_FEATURE = "use_window_feature"
|
||||
CONF_USE_MOTION_FEATURE = "use_motion_feature"
|
||||
CONF_USE_PRESENCE_FEATURE = "use_presence_feature"
|
||||
CONF_USE_POWER_FEATURE = "use_power_feature"
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE = "use_central_boiler_feature"
|
||||
CONF_USE_AUTO_START_STOP_FEATURE = "use_auto_start_stop_feature"
|
||||
CONF_AC_MODE = "ac_mode"
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
|
||||
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
|
||||
CONF_VALVE = "valve_entity_id"
|
||||
CONF_VALVE_2 = "valve_entity2_id"
|
||||
CONF_VALVE_3 = "valve_entity3_id"
|
||||
CONF_VALVE_4 = "valve_entity4_id"
|
||||
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"
|
||||
@@ -123,10 +120,28 @@ 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"
|
||||
CONF_HEATER_2 = "heater_entity2_id"
|
||||
CONF_HEATER_3 = "heater_entity3_id"
|
||||
CONF_HEATER_4 = "heater_entity4_id"
|
||||
CONF_CLIMATE = "climate_entity_id"
|
||||
CONF_CLIMATE_2 = "climate_entity2_id"
|
||||
CONF_CLIMATE_3 = "climate_entity3_id"
|
||||
CONF_CLIMATE_4 = "climate_entity4_id"
|
||||
CONF_VALVE = "valve_entity_id"
|
||||
CONF_VALVE_2 = "valve_entity2_id"
|
||||
CONF_VALVE_3 = "valve_entity3_id"
|
||||
CONF_VALVE_4 = "valve_entity4_id"
|
||||
|
||||
# Global params into configuration.yaml
|
||||
CONF_SHORT_EMA_PARAMS = "short_ema_params"
|
||||
CONF_SAFETY_MODE = "safety_mode"
|
||||
CONF_MAX_ON_PERCENT = "max_on_percent"
|
||||
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config"
|
||||
CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config"
|
||||
@@ -145,6 +160,36 @@ CONF_CENTRAL_BOILER_DEACTIVATION_SRV = "central_boiler_deactivation_service"
|
||||
CONF_USED_BY_CENTRAL_BOILER = "used_by_controls_central_boiler"
|
||||
CONF_WINDOW_ACTION = "window_action"
|
||||
|
||||
CONF_AUTO_START_STOP_LEVEL = "auto_start_stop_level"
|
||||
AUTO_START_STOP_LEVEL_NONE = "auto_start_stop_none"
|
||||
AUTO_START_STOP_LEVEL_SLOW = "auto_start_stop_slow"
|
||||
AUTO_START_STOP_LEVEL_MEDIUM = "auto_start_stop_medium"
|
||||
AUTO_START_STOP_LEVEL_FAST = "auto_start_stop_fast"
|
||||
CONF_AUTO_START_STOP_LEVELS = [
|
||||
AUTO_START_STOP_LEVEL_NONE,
|
||||
AUTO_START_STOP_LEVEL_SLOW,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM,
|
||||
AUTO_START_STOP_LEVEL_FAST,
|
||||
]
|
||||
|
||||
# For explicit typing purpose only
|
||||
TYPE_AUTO_START_STOP_LEVELS = Literal[ # pylint: disable=invalid-name
|
||||
AUTO_START_STOP_LEVEL_FAST,
|
||||
AUTO_START_STOP_LEVEL_MEDIUM,
|
||||
AUTO_START_STOP_LEVEL_SLOW,
|
||||
AUTO_START_STOP_LEVEL_NONE,
|
||||
]
|
||||
|
||||
HVAC_OFF_REASON_NAME = "hvac_off_reason"
|
||||
HVAC_OFF_REASON_MANUAL = "manual"
|
||||
HVAC_OFF_REASON_AUTO_START_STOP = "auto_start_stop"
|
||||
HVAC_OFF_REASON_WINDOW_DETECTION = "window_detection"
|
||||
HVAC_OFF_REASONS = Literal[ # pylint: disable=invalid-name
|
||||
HVAC_OFF_REASON_MANUAL,
|
||||
HVAC_OFF_REASON_AUTO_START_STOP,
|
||||
HVAC_OFF_REASON_WINDOW_DETECTION,
|
||||
]
|
||||
|
||||
DEFAULT_SHORT_EMA_PARAMS = {
|
||||
"max_alpha": 0.5,
|
||||
# In sec
|
||||
@@ -216,10 +261,6 @@ CONF_PRESETS_AWAY_WITH_AC_VALUES = list(CONF_PRESETS_AWAY_WITH_AC.values())
|
||||
ALL_CONF = (
|
||||
[
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_HEATER_KEEP_ALIVE,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
@@ -249,20 +290,12 @@ ALL_CONF = (
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_USE_WINDOW_FEATURE,
|
||||
CONF_USE_MOTION_FEATURE,
|
||||
CONF_USE_PRESENCE_FEATURE,
|
||||
CONF_USE_POWER_FEATURE,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
CONF_AC_MODE,
|
||||
CONF_VALVE,
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
@@ -296,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,
|
||||
@@ -330,7 +364,11 @@ CONF_WINDOW_ACTIONS = [
|
||||
CONF_WINDOW_ECO_TEMP,
|
||||
]
|
||||
|
||||
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
SUPPORT_FLAGS = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
SERVICE_SET_PRESENCE = "set_presence"
|
||||
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
|
||||
@@ -430,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):
|
||||
@@ -445,6 +483,7 @@ class EventType(Enum):
|
||||
CENTRAL_BOILER_EVENT: str = "versatile_thermostat_central_boiler_event"
|
||||
PRESET_EVENT: str = "versatile_thermostat_preset_event"
|
||||
WINDOW_AUTO_EVENT: str = "versatile_thermostat_window_auto_event"
|
||||
AUTO_START_STOP_EVENT: str = "versatile_thermostat_auto_start_stop_event"
|
||||
|
||||
|
||||
def send_vtherm_event(hass, event_type: EventType, entity, data: dict):
|
||||
@@ -456,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."""
|
||||
|
||||
@@ -476,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"""
|
||||
|
||||
|
||||
18
custom_components/versatile_thermostat/icons.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"entity": {
|
||||
"climate": {
|
||||
"versatile_thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"shedding": "mdi:power-plug-off",
|
||||
"safety": "mdi:shield-alert",
|
||||
"none": "mdi:knob",
|
||||
"frost": "mdi:snowflake"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "6.3.0",
|
||||
"version": "6.8.3",
|
||||
"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
|
||||
|
||||
@@ -31,6 +31,7 @@ class PropAlgorithm:
|
||||
cycle_min: int,
|
||||
minimal_activation_delay: int,
|
||||
vtherm_entity_id: str = None,
|
||||
max_on_percent: float = None,
|
||||
) -> None:
|
||||
"""Initialisation of the Proportional Algorithm"""
|
||||
_LOGGER.debug(
|
||||
@@ -78,6 +79,7 @@ class PropAlgorithm:
|
||||
self._off_time_sec = self._cycle_min * 60
|
||||
self._security = False
|
||||
self._default_on_percent = 0
|
||||
self._max_on_percent = max_on_percent
|
||||
|
||||
def calculate(
|
||||
self,
|
||||
@@ -161,6 +163,15 @@ class PropAlgorithm:
|
||||
)
|
||||
self._on_percent = self._calculated_on_percent
|
||||
|
||||
if self._max_on_percent is not None and self._on_percent > self._max_on_percent:
|
||||
_LOGGER.debug(
|
||||
"%s - Heating period clamped to %s (instead of %s) due to max_on_percent setting.",
|
||||
self._vtherm_entity_id,
|
||||
self._max_on_percent,
|
||||
self._on_percent,
|
||||
)
|
||||
self._on_percent = self._max_on_percent
|
||||
|
||||
self._on_time_sec = self._on_percent * self._cycle_min * 60
|
||||
|
||||
# Do not heat for less than xx sec
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"power": "Power management",
|
||||
"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"
|
||||
}
|
||||
@@ -63,28 +65,18 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "1st heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1st underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode",
|
||||
"valve_entity_id": "1st valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
@@ -93,21 +85,10 @@
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not required",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not required",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not required",
|
||||
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"valve_entity_id": "1st valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
@@ -223,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": {
|
||||
@@ -262,6 +271,8 @@
|
||||
"power": "Power management",
|
||||
"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"
|
||||
}
|
||||
@@ -298,28 +309,18 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entities - {name}",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "1st heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1st underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode",
|
||||
"valve_entity_id": "1st valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
@@ -328,21 +329,10 @@
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"valve_entity_id": "1st valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
@@ -458,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": {
|
||||
@@ -465,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"
|
||||
@@ -487,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": {
|
||||
@@ -514,6 +534,14 @@
|
||||
"comfort": "Comfort",
|
||||
"boost": "Boost"
|
||||
}
|
||||
},
|
||||
"auto_start_stop": {
|
||||
"options": {
|
||||
"auto_start_stop_none": "No auto start/stop",
|
||||
"auto_start_stop_slow": "Slow detection",
|
||||
"auto_start_stop_medium": "Medium detection",
|
||||
"auto_start_stop_fast": "Fast detection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -524,7 +552,8 @@
|
||||
"state": {
|
||||
"power": "Shedding",
|
||||
"security": "Safety",
|
||||
"none": "Manual"
|
||||
"none": "Manual",
|
||||
"frost": "Frost"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
168
custom_components/versatile_thermostat/switch.py
Normal file
@@ -0,0 +1,168 @@
|
||||
## pylint: disable=unused-argument
|
||||
|
||||
""" Implements the VersatileThermostat select component """
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .commons import VersatileThermostatBaseEntity
|
||||
|
||||
from .const import * # pylint: disable=unused-wildcard-import,wildcard-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the VersatileThermostat switches with config flow."""
|
||||
_LOGGER.debug(
|
||||
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
|
||||
)
|
||||
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
auto_start_stop_feature = entry.data.get(CONF_USE_AUTO_START_STOP_FEATURE)
|
||||
|
||||
entities = []
|
||||
if vt_type == CONF_THERMOSTAT_CLIMATE:
|
||||
entities.append(FollowUnderlyingTemperatureChange(hass, unique_id, name, entry))
|
||||
|
||||
if auto_start_stop_feature is True:
|
||||
# Creates a switch to enable the auto-start/stop
|
||||
enable_entity = AutoStartStopEnable(hass, unique_id, name, entry)
|
||||
entities.append(enable_entity)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity):
|
||||
"""The that enables the ManagedDevice optimisation with"""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigEntry
|
||||
):
|
||||
super().__init__(hass, unique_id, name)
|
||||
self._attr_name = "Enable auto start/stop"
|
||||
self._attr_unique_id = f"{self._device_name}_enable_auto_start_stop"
|
||||
self._default_value = (
|
||||
entry_infos.data.get(CONF_AUTO_START_STOP_LEVEL)
|
||||
!= AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
self._attr_is_on = self._default_value
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""The icon"""
|
||||
return "mdi:power-sleep"
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Récupérer le dernier état sauvegardé de l'entité
|
||||
last_state = await self.async_get_last_state()
|
||||
|
||||
# Si l'état précédent existe, vous pouvez l'utiliser
|
||||
if last_state is not None:
|
||||
self._attr_is_on = last_state.state == "on"
|
||||
else:
|
||||
# If no previous state set it to false by default
|
||||
self._attr_is_on = self._default_value
|
||||
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
def update_my_state_and_vtherm(self):
|
||||
"""Update the auto_start_stop_enable flag in my VTherm"""
|
||||
self.async_write_ha_state()
|
||||
if self.my_climate is not None:
|
||||
self.my_climate.set_auto_start_stop_enable(self._attr_is_on)
|
||||
|
||||
@callback
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
self.turn_on()
|
||||
|
||||
@callback
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
self.turn_off()
|
||||
|
||||
@overrides
|
||||
def turn_off(self, **kwargs: Any):
|
||||
self._attr_is_on = False
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
@overrides
|
||||
def turn_on(self, **kwargs: Any):
|
||||
self._attr_is_on = True
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
|
||||
class FollowUnderlyingTemperatureChange(
|
||||
VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity
|
||||
):
|
||||
"""The that enables the ManagedDevice optimisation with"""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigEntry
|
||||
):
|
||||
super().__init__(hass, unique_id, name)
|
||||
self._attr_name = "Follow underlying temp change"
|
||||
self._attr_unique_id = f"{self._device_name}_follow_underlying_temp_change"
|
||||
self._attr_is_on = False
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""The icon"""
|
||||
return "mdi:content-copy"
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Récupérer le dernier état sauvegardé de l'entité
|
||||
last_state = await self.async_get_last_state()
|
||||
|
||||
# Si l'état précédent existe, vous pouvez l'utiliser
|
||||
if last_state is not None:
|
||||
self._attr_is_on = last_state.state == "on"
|
||||
else:
|
||||
# If no previous state set it to false by default
|
||||
self._attr_is_on = False
|
||||
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
def update_my_state_and_vtherm(self):
|
||||
"""Update the follow flag in my VTherm"""
|
||||
self.async_write_ha_state()
|
||||
if self.my_climate is not None:
|
||||
self.my_climate.set_follow_underlying_temp_change(self._attr_is_on)
|
||||
|
||||
@callback
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
self.turn_on()
|
||||
|
||||
@callback
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
self.turn_off()
|
||||
|
||||
@overrides
|
||||
def turn_off(self, **kwargs: Any):
|
||||
self._attr_is_on = False
|
||||
self.update_my_state_and_vtherm()
|
||||
|
||||
@overrides
|
||||
def turn_on(self, **kwargs: Any):
|
||||
self._attr_is_on = True
|
||||
self.update_my_state_and_vtherm()
|
||||
@@ -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,44 +16,19 @@ 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
|
||||
|
||||
from .const import (
|
||||
overrides,
|
||||
DOMAIN,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_NONE,
|
||||
CONF_AUTO_REGULATION_SLOW,
|
||||
CONF_AUTO_REGULATION_LIGHT,
|
||||
CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP,
|
||||
CONF_AUTO_FAN_MODE,
|
||||
CONF_AUTO_FAN_NONE,
|
||||
CONF_AUTO_FAN_LOW,
|
||||
CONF_AUTO_FAN_MEDIUM,
|
||||
CONF_AUTO_FAN_HIGH,
|
||||
CONF_AUTO_FAN_TURBO,
|
||||
RegulationParamSlow,
|
||||
RegulationParamLight,
|
||||
RegulationParamMedium,
|
||||
RegulationParamStrong,
|
||||
AUTO_FAN_DTEMP_THRESHOLD,
|
||||
AUTO_FAN_DEACTIVATED_MODES,
|
||||
UnknownEntity,
|
||||
)
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .underlyings import UnderlyingClimate
|
||||
from .auto_start_stop_algorithm import (
|
||||
AutoStartStopDetectionAlgorithm,
|
||||
AUTO_START_STOP_ACTION_OFF,
|
||||
AUTO_START_STOP_ACTION_ON,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,43 +39,30 @@ HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||
HVACAction.HEATING,
|
||||
]
|
||||
|
||||
|
||||
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
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
{
|
||||
"is_over_climate",
|
||||
"start_hvac_action_date",
|
||||
"underlying_climate_0",
|
||||
"underlying_climate_1",
|
||||
"underlying_climate_2",
|
||||
"underlying_climate_3",
|
||||
"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",
|
||||
}
|
||||
)
|
||||
_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",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -108,25 +70,97 @@ 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):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
|
||||
for climate in config_entry.get(CONF_UNDERLYING_LIST):
|
||||
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)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_MODE) is not None
|
||||
else CONF_AUTO_REGULATION_NONE
|
||||
)
|
||||
|
||||
self._auto_regulation_dtemp = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||
else 0.5
|
||||
)
|
||||
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 5
|
||||
)
|
||||
|
||||
self._auto_fan_mode = (
|
||||
config_entry.get(CONF_AUTO_FAN_MODE)
|
||||
if config_entry.get(CONF_AUTO_FAN_MODE) is not None
|
||||
else CONF_AUTO_FAN_NONE
|
||||
)
|
||||
|
||||
self._auto_regulation_use_device_temp = config_entry.get(
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
|
||||
)
|
||||
|
||||
use_auto_start_stop = config_entry.get(CONF_USE_AUTO_START_STOP_FEATURE, False)
|
||||
if use_auto_start_stop:
|
||||
self._auto_start_stop_level = config_entry.get(
|
||||
CONF_AUTO_START_STOP_LEVEL, AUTO_START_STOP_LEVEL_NONE
|
||||
)
|
||||
else:
|
||||
self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE
|
||||
|
||||
# Instanciate the auto start stop algo
|
||||
self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm(
|
||||
self._auto_start_stop_level, self.name
|
||||
)
|
||||
|
||||
@property
|
||||
def is_over_climate(self) -> bool:
|
||||
"""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,
|
||||
@@ -138,12 +172,18 @@ 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)
|
||||
# Is necessary cause control_heating method will not force the update.
|
||||
await self._send_regulated_temperature(force=True)
|
||||
|
||||
async def _send_regulated_temperature(self, force=False):
|
||||
@@ -169,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
|
||||
@@ -216,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
|
||||
@@ -228,17 +270,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
and self.auto_regulation_use_device_temp
|
||||
# and we have access to the device temp
|
||||
and (device_temp := under.underlying_current_temperature) is not None
|
||||
# and target is not reach (ie we need regulation)
|
||||
and (
|
||||
(
|
||||
self.hvac_mode == HVACMode.COOL
|
||||
and self.target_temperature < self.current_temperature
|
||||
)
|
||||
or (
|
||||
self.hvac_mode == HVACMode.HEAT
|
||||
and self.target_temperature > self.current_temperature
|
||||
)
|
||||
)
|
||||
):
|
||||
offset_temp = device_temp - self.current_temperature
|
||||
|
||||
@@ -303,53 +334,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
)
|
||||
await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
for climate in [
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
]:
|
||||
if config_entry.get(climate):
|
||||
self._underlyings.append(
|
||||
UnderlyingClimate(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
climate_entity_id=config_entry.get(climate),
|
||||
)
|
||||
)
|
||||
|
||||
self.choose_auto_regulation_mode(
|
||||
config_entry.get(CONF_AUTO_REGULATION_MODE)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_MODE) is not None
|
||||
else CONF_AUTO_REGULATION_NONE
|
||||
)
|
||||
|
||||
self._auto_regulation_dtemp = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||
else 0.5
|
||||
)
|
||||
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 5
|
||||
)
|
||||
|
||||
self._auto_fan_mode = (
|
||||
config_entry.get(CONF_AUTO_FAN_MODE)
|
||||
if config_entry.get(CONF_AUTO_FAN_MODE) is not None
|
||||
else CONF_AUTO_FAN_NONE
|
||||
)
|
||||
|
||||
self._auto_regulation_use_device_temp = config_entry.get(
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
|
||||
)
|
||||
|
||||
def choose_auto_regulation_mode(self, auto_regulation_mode: str):
|
||||
"""Choose or change the regulation mode"""
|
||||
self._auto_regulation_mode = auto_regulation_mode
|
||||
@@ -520,18 +504,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
self._attr_extra_state_attributes["start_hvac_action_date"] = (
|
||||
self._underlying_climate_start_hvac_action_date
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
|
||||
0
|
||||
].entity_id
|
||||
self._attr_extra_state_attributes["underlying_climate_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_climate_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_climate_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["underlying_entities"] = [
|
||||
underlying.entity_id for underlying in self._underlyings
|
||||
]
|
||||
|
||||
if self.is_regulated:
|
||||
self._attr_extra_state_attributes["is_regulated"] = self.is_regulated
|
||||
@@ -562,7 +538,34 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
self.auto_regulation_use_device_temp
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["auto_start_stop_enable"] = (
|
||||
self.auto_start_stop_enable
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["auto_start_stop_level"] = (
|
||||
self._auto_start_stop_algo.level
|
||||
)
|
||||
self._attr_extra_state_attributes["auto_start_stop_dtmin"] = (
|
||||
self._auto_start_stop_algo.dt_min
|
||||
)
|
||||
self._attr_extra_state_attributes["auto_start_stop_accumulated_error"] = (
|
||||
self._auto_start_stop_algo.accumulated_error
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"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
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Calling update_custom_attributes: %s",
|
||||
self,
|
||||
@@ -609,8 +612,18 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER.debug(
|
||||
"%s - incremente_energy incremented energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - added energy is %.3f . Total energy is now: %.3f",
|
||||
@@ -692,8 +705,9 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
else None
|
||||
)
|
||||
|
||||
last_sent_temperature = under.last_sent_temperature or 0
|
||||
under_temp_diff = (
|
||||
(new_target_temp - under.last_sent_temperature) if new_target_temp else 0
|
||||
(new_target_temp - last_sent_temperature) if new_target_temp else 0
|
||||
)
|
||||
if -1 < under_temp_diff < 1:
|
||||
under_temp_diff = 0
|
||||
@@ -717,6 +731,23 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
)
|
||||
return
|
||||
|
||||
# Ignore new target temperature when out of range
|
||||
if (
|
||||
not new_target_temp is None
|
||||
and not self._attr_min_temp is None
|
||||
and not self._attr_max_temp is None
|
||||
and not (self._attr_min_temp <= new_target_temp <= self._attr_max_temp)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - underlying sent a target temperature (%s) which is out of configured min/max range (%s / %s). The value will be ignored",
|
||||
self,
|
||||
new_target_temp,
|
||||
self._attr_min_temp,
|
||||
self._attr_max_temp,
|
||||
)
|
||||
new_target_temp = None
|
||||
under_temp_diff = 0
|
||||
|
||||
# A real changes have to be managed
|
||||
_LOGGER.info(
|
||||
"%s - Underlying climate %s have changed. new_hvac_mode is %s (vs %s), new_hvac_action=%s (vs %s), new_target_temp=%s (vs %s), new_fan_mode=%s (vs %s)",
|
||||
@@ -834,7 +865,12 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
changes = True
|
||||
|
||||
# try to manage new target temperature set if state if no other changes have been found
|
||||
if not changes:
|
||||
# and if a target temperature have already been sent
|
||||
if (
|
||||
self._follow_underlying_temp_change
|
||||
and not changes
|
||||
and under.last_sent_temperature is not None
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s",
|
||||
under.last_sent_temperature,
|
||||
@@ -858,11 +894,92 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
|
||||
await end_climate_changed(changes)
|
||||
|
||||
async def check_auto_start_stop(self):
|
||||
"""Check the auto-start-stop and an eventual action
|
||||
Return False if we should stop the control_heating method"""
|
||||
slope = (self.last_temperature_slope or 0) / 60 # to have the slope in °/min
|
||||
action = self._auto_start_stop_algo.calculate_action(
|
||||
self.hvac_mode,
|
||||
self._saved_hvac_mode,
|
||||
self.target_temperature,
|
||||
self.current_temperature,
|
||||
slope,
|
||||
self.now,
|
||||
)
|
||||
_LOGGER.debug("%s - auto_start_stop action is %s", self, action)
|
||||
if action == AUTO_START_STOP_ACTION_OFF and self.is_on:
|
||||
_LOGGER.info(
|
||||
"%s - Turning OFF the Vtherm due to auto-start-stop conditions",
|
||||
self,
|
||||
)
|
||||
self.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP)
|
||||
await self.async_turn_off()
|
||||
|
||||
# Send an event
|
||||
self.send_event(
|
||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
||||
data={
|
||||
"type": "stop",
|
||||
"name": self.name,
|
||||
"cause": "Auto stop conditions reached",
|
||||
"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(slope, 3),
|
||||
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
|
||||
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
|
||||
},
|
||||
)
|
||||
|
||||
# Stop here
|
||||
return False
|
||||
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
|
||||
)
|
||||
await 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 conditions reached",
|
||||
"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(slope, 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()
|
||||
|
||||
return True
|
||||
|
||||
@overrides
|
||||
async def async_control_heating(self, force=False, _=None) -> bool:
|
||||
"""The main function used to run the calculation at each cycle"""
|
||||
ret = await super().async_control_heating(force, _)
|
||||
|
||||
# Check if we need to auto start/stop the Vtherm
|
||||
if self.auto_start_stop_enable:
|
||||
continu = await self.check_auto_start_stop()
|
||||
if not continu:
|
||||
return ret
|
||||
else:
|
||||
_LOGGER.debug("%s - auto start/stop is disabled", self)
|
||||
|
||||
# Continue the normal async_control_heating
|
||||
|
||||
# Send the regulated temperature to the underlyings
|
||||
await self._send_regulated_temperature()
|
||||
|
||||
if self._auto_fan_mode and self._auto_fan_mode != CONF_AUTO_FAN_NONE:
|
||||
@@ -870,6 +987,42 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
|
||||
return ret
|
||||
|
||||
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):
|
||||
"""Set the flaf follow the underlying temperature changes"""
|
||||
self._follow_underlying_temp_change = follow
|
||||
self.update_custom_attributes()
|
||||
|
||||
@property
|
||||
def auto_regulation_mode(self) -> str | None:
|
||||
"""Get the regulation mode"""
|
||||
@@ -966,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.
|
||||
@@ -997,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.
|
||||
@@ -1016,6 +1168,21 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def auto_start_stop_level(self) -> TYPE_AUTO_START_STOP_LEVELS:
|
||||
"""Return the auto start/stop level."""
|
||||
return self._auto_start_stop_level
|
||||
|
||||
@property
|
||||
def auto_start_stop_enable(self) -> bool:
|
||||
"""Returns the auto_start_stop_enable"""
|
||||
return self._is_auto_start_stop_enabled
|
||||
|
||||
@property
|
||||
def follow_underlying_temp_change(self) -> bool:
|
||||
"""Get the follow underlying temp change flag"""
|
||||
return self._follow_underlying_temp_change
|
||||
|
||||
@overrides
|
||||
def init_underlyings(self):
|
||||
"""Init the underlyings if not already done"""
|
||||
@@ -1118,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,13 +7,11 @@ 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 (
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_UNDERLYING_LIST,
|
||||
CONF_HEATER_KEEP_ALIVE,
|
||||
CONF_INVERSE_SWITCH,
|
||||
overrides,
|
||||
@@ -25,7 +23,6 @@ from .prop_algorithm import PropAlgorithm
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
"""Representation of a base class for a Versatile Thermostat over a switch."""
|
||||
|
||||
@@ -35,10 +32,7 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
{
|
||||
"is_over_switch",
|
||||
"is_inversed",
|
||||
"underlying_switch_0",
|
||||
"underlying_switch_1",
|
||||
"underlying_switch_2",
|
||||
"underlying_switch_3",
|
||||
"underlying_entities",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
@@ -46,16 +40,16 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"power_percent",
|
||||
"calculated_on_percent",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# 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:
|
||||
@@ -88,15 +82,10 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
self._cycle_min,
|
||||
self._minimal_activation_delay,
|
||||
self.name,
|
||||
max_on_percent=self._max_on_percent,
|
||||
)
|
||||
|
||||
lst_switches = [config_entry.get(CONF_HEATER)]
|
||||
if config_entry.get(CONF_HEATER_2):
|
||||
lst_switches.append(config_entry.get(CONF_HEATER_2))
|
||||
if config_entry.get(CONF_HEATER_3):
|
||||
lst_switches.append(config_entry.get(CONF_HEATER_3))
|
||||
if config_entry.get(CONF_HEATER_4):
|
||||
lst_switches.append(config_entry.get(CONF_HEATER_4))
|
||||
lst_switches = config_entry.get(CONF_UNDERLYING_LIST)
|
||||
|
||||
delta_cycle = self._cycle_min * 60 / len(lst_switches)
|
||||
for idx, switch in enumerate(lst_switches):
|
||||
@@ -140,16 +129,10 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch
|
||||
self._attr_extra_state_attributes["is_inversed"] = self.is_inversed
|
||||
self._attr_extra_state_attributes["keep_alive_sec"] = under0.keep_alive_sec
|
||||
self._attr_extra_state_attributes["underlying_switch_0"] = under0.entity_id
|
||||
self._attr_extra_state_attributes["underlying_switch_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_switch_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_switch_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["underlying_entities"] = [
|
||||
underlying.entity_id for underlying in self._underlyings
|
||||
]
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"on_percent"
|
||||
@@ -165,6 +148,9 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
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[
|
||||
"calculated_on_percent"
|
||||
] = self._prop_algorithm.calculated_on_percent
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -201,8 +187,18 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER.debug(
|
||||
"%s - incremente_energy increment energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -15,10 +15,7 @@ from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
from .const import (
|
||||
CONF_VALVE,
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
CONF_UNDERLYING_LIST,
|
||||
# This is not really self-regulation but regulation here
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
@@ -29,7 +26,6 @@ from .underlyings import UnderlyingValve
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method
|
||||
"""Representation of a class for a Versatile Thermostat over a Valve"""
|
||||
|
||||
@@ -37,10 +33,7 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
frozenset(
|
||||
{
|
||||
"is_over_valve",
|
||||
"underlying_valve_0",
|
||||
"underlying_valve_1",
|
||||
"underlying_valve_2",
|
||||
"underlying_valve_3",
|
||||
"underlying_entities",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
@@ -50,6 +43,7 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
"auto_regulation_dpercent",
|
||||
"auto_regulation_period_min",
|
||||
"last_calculation_timestamp",
|
||||
"calculated_on_percent",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -103,15 +97,10 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
self._cycle_min,
|
||||
self._minimal_activation_delay,
|
||||
self.name,
|
||||
max_on_percent=self._max_on_percent,
|
||||
)
|
||||
|
||||
lst_valves = [config_entry.get(CONF_VALVE)]
|
||||
if config_entry.get(CONF_VALVE_2):
|
||||
lst_valves.append(config_entry.get(CONF_VALVE_2))
|
||||
if config_entry.get(CONF_VALVE_3):
|
||||
lst_valves.append(config_entry.get(CONF_VALVE_3))
|
||||
if config_entry.get(CONF_VALVE_4):
|
||||
lst_valves.append(config_entry.get(CONF_VALVE_4))
|
||||
lst_valves = config_entry.get(CONF_UNDERLYING_LIST)
|
||||
|
||||
for _, valve in enumerate(lst_valves):
|
||||
self._underlyings.append(
|
||||
@@ -163,18 +152,10 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
"valve_open_percent"
|
||||
] = self.valve_open_percent
|
||||
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
|
||||
self._attr_extra_state_attributes["underlying_valve_0"] = self._underlyings[
|
||||
0
|
||||
].entity_id
|
||||
self._attr_extra_state_attributes["underlying_valve_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_valve_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_valve_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["underlying_entities"] = [
|
||||
underlying.entity_id for underlying in self._underlyings
|
||||
]
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"on_percent"
|
||||
@@ -200,6 +181,9 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
if self._last_calculation_timestamp
|
||||
else None
|
||||
)
|
||||
self._attr_extra_state_attributes[
|
||||
"calculated_on_percent"
|
||||
] = self._prop_algorithm.calculated_on_percent
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -264,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
|
||||
|
||||
@@ -285,8 +270,18 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
|
||||
if self._total_energy is None:
|
||||
self._total_energy = added_energy
|
||||
_LOGGER.debug(
|
||||
"%s - incremente_energy set energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
_LOGGER.debug(
|
||||
"%s - get_my_previous_state increment energy is %s",
|
||||
self,
|
||||
self._total_energy,
|
||||
)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"power": "Power management",
|
||||
"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"
|
||||
}
|
||||
@@ -63,28 +65,18 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Linked entities",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "1st heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1st underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode",
|
||||
"valve_entity_id": "1st valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
@@ -93,21 +85,10 @@
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not required",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not required",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not required",
|
||||
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"valve_entity_id": "1st valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
@@ -223,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": {
|
||||
@@ -262,6 +271,8 @@
|
||||
"power": "Power management",
|
||||
"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"
|
||||
}
|
||||
@@ -298,28 +309,18 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Entities - {name}",
|
||||
"description": "Linked entities attributes",
|
||||
"data": {
|
||||
"heater_entity_id": "1st heater switch",
|
||||
"heater_entity2_id": "2nd heater switch",
|
||||
"heater_entity3_id": "3rd heater switch",
|
||||
"heater_entity4_id": "4th heater switch",
|
||||
"underlying_entity_ids": "The device(s) to be controlled",
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"climate_entity_id": "1st underlying climate",
|
||||
"climate_entity2_id": "2nd underlying climate",
|
||||
"climate_entity3_id": "3rd underlying climate",
|
||||
"climate_entity4_id": "4th underlying climate",
|
||||
"ac_mode": "AC mode",
|
||||
"valve_entity_id": "1st valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
@@ -328,21 +329,10 @@
|
||||
"auto_fan_mode": "Auto fan mode"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||
"underlying_entity_ids": "The device(s) to be controlled - 1 is required",
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"climate_entity_id": "Underlying climate entity id",
|
||||
"climate_entity2_id": "2nd underlying climate entity id",
|
||||
"climate_entity3_id": "3rd underlying climate entity id",
|
||||
"climate_entity4_id": "4th underlying climate entity id",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"valve_entity_id": "1st valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
@@ -458,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": {
|
||||
@@ -465,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"
|
||||
@@ -487,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": {
|
||||
@@ -514,6 +534,14 @@
|
||||
"comfort": "Comfort",
|
||||
"boost": "Boost"
|
||||
}
|
||||
},
|
||||
"auto_start_stop": {
|
||||
"options": {
|
||||
"auto_start_stop_none": "No auto start/stop",
|
||||
"auto_start_stop_slow": "Slow detection",
|
||||
"auto_start_stop_medium": "Medium detection",
|
||||
"auto_start_stop_fast": "Fast detection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -524,7 +552,8 @@
|
||||
"state": {
|
||||
"power": "Shedding",
|
||||
"security": "Safety",
|
||||
"none": "Manual"
|
||||
"none": "Manual",
|
||||
"frost": "Frost"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"power": "Gestion de la puissance",
|
||||
"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"
|
||||
}
|
||||
@@ -63,55 +65,34 @@
|
||||
"use_motion_feature": "Avec détection de mouvement",
|
||||
"use_power_feature": "Avec gestion de la puissance",
|
||||
"use_presence_feature": "Avec détection de présence",
|
||||
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante."
|
||||
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
|
||||
"use_auto_start_stop_feature": "Avec démarrage et extinction automatique"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"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"
|
||||
}
|
||||
@@ -235,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",
|
||||
"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": {
|
||||
@@ -274,6 +271,8 @@
|
||||
"power": "Gestion de la puissance",
|
||||
"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"
|
||||
}
|
||||
@@ -310,55 +309,34 @@
|
||||
"use_motion_feature": "Avec détection de mouvement",
|
||||
"use_power_feature": "Avec gestion de la puissance",
|
||||
"use_presence_feature": "Avec détection de présence",
|
||||
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante."
|
||||
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
|
||||
"use_auto_start_stop_feature": "Avec démarrage et extinction automatique"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
@@ -476,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": {
|
||||
@@ -483,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é"
|
||||
@@ -505,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": {
|
||||
@@ -532,6 +528,14 @@
|
||||
"comfort": "Confort",
|
||||
"boost": "Renforcé (boost)"
|
||||
}
|
||||
},
|
||||
"auto_start_stop": {
|
||||
"options": {
|
||||
"auto_start_stop_none": "No auto start/stop",
|
||||
"auto_start_stop_slow": "Slow detection",
|
||||
"auto_start_stop_medium": "Medium detection",
|
||||
"auto_start_stop_fast": "Fast detection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -542,7 +546,8 @@
|
||||
"state": {
|
||||
"power": "Délestage",
|
||||
"security": "Sécurité",
|
||||
"none": "Manuel"
|
||||
"none": "Manuel",
|
||||
"frost": "Hors Gel"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +364,8 @@
|
||||
"state": {
|
||||
"power": "Ripartizione",
|
||||
"security": "Sicurezza",
|
||||
"none": "Manuale"
|
||||
"none": "Manuale",
|
||||
"frost": "Gelo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -550,14 +564,11 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
def is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
if self.is_initialized:
|
||||
return (
|
||||
self._underlying_climate.hvac_mode != HVACMode.OFF
|
||||
and self.hvac_action
|
||||
not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
]
|
||||
)
|
||||
return self.hvac_mode != HVACMode.OFF and self.hvac_action not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
None,
|
||||
]
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -716,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"""
|
||||
@@ -850,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,
|
||||
@@ -865,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,
|
||||
@@ -886,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)
|
||||
@@ -897,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"""
|
||||
@@ -935,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:
|
||||
@@ -972,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:
|
||||
@@ -986,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
|
||||
|
||||
@@ -15,6 +15,7 @@ from .const import (
|
||||
CONF_SAFETY_MODE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_MAX_ON_PERCENT,
|
||||
)
|
||||
|
||||
VTHERM_API_NAME = "vtherm_api"
|
||||
@@ -60,6 +61,7 @@ class VersatileThermostatAPI(dict):
|
||||
self._central_mode_select = None
|
||||
# A dict that will store all Number entities which holds the temperature
|
||||
self._number_temperatures = dict()
|
||||
self._max_on_percent = None
|
||||
|
||||
def find_central_configuration(self):
|
||||
"""Search for a central configuration"""
|
||||
@@ -107,6 +109,12 @@ class VersatileThermostatAPI(dict):
|
||||
if self._safety_mode:
|
||||
_LOGGER.debug("We have found safet_mode params %s", self._safety_mode)
|
||||
|
||||
self._max_on_percent = config.get(CONF_MAX_ON_PERCENT)
|
||||
if self._max_on_percent:
|
||||
_LOGGER.debug(
|
||||
"We have found max_on_percent setting %s", self._max_on_percent
|
||||
)
|
||||
|
||||
def register_central_boiler(self, central_boiler_entity):
|
||||
"""Register the central boiler entity. This is used by the CentralBoilerBinarySensor
|
||||
class to register itself at creation"""
|
||||
@@ -150,10 +158,11 @@ class VersatileThermostatAPI(dict):
|
||||
return entity.state
|
||||
return None
|
||||
|
||||
async def init_vtherm_links(self):
|
||||
async def init_vtherm_links(self, entry_id=None):
|
||||
"""Initialize all VTherms entities links
|
||||
This method is called when HA is fully started (and all entities should be initialized)
|
||||
Or when we need to reload all VTherm links (with Number temp entities, central boiler, ...)
|
||||
If entry_id is set, only the VTherm of this entry will be reloaded
|
||||
"""
|
||||
await self.reload_central_boiler_binary_listener()
|
||||
await self.reload_central_boiler_entities_list()
|
||||
@@ -170,12 +179,14 @@ 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
|
||||
):
|
||||
await entity.async_startup(self.find_central_configuration())
|
||||
if entry_id is None or entry_id == entity.unique_id:
|
||||
await entity.async_startup(self.find_central_configuration())
|
||||
|
||||
async def init_vtherm_preset_with_central(self):
|
||||
"""Init all VTherm presets when the VTherm uses central temperature"""
|
||||
@@ -239,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 |