Compare commits
1 Commits
3.1.1
...
3.1.0.alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ffa6d77e6 |
@@ -64,10 +64,8 @@ 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",
|
||||
event.origin.name if event and event.origin else None,
|
||||
)
|
||||
_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
|
||||
if old_state != self._attr_is_on:
|
||||
@@ -99,10 +97,8 @@ 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",
|
||||
event.origin.name if event and event.origin else None,
|
||||
)
|
||||
_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
|
||||
if old_state != self._attr_is_on:
|
||||
@@ -134,12 +130,13 @@ 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",
|
||||
event.origin.name if event and event.origin else None,
|
||||
)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
old_state = self._attr_is_on
|
||||
self._attr_is_on = self.my_climate.window_state == STATE_ON
|
||||
self._attr_is_on = (
|
||||
self.my_climate.window_state == STATE_ON
|
||||
or self.my_climate.window_auto_state == STATE_ON
|
||||
)
|
||||
if old_state != self._attr_is_on:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
@@ -151,7 +148,10 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
if self._attr_is_on:
|
||||
return "mdi:window-open-variant"
|
||||
if self.my_climate.window_state == STATE_ON:
|
||||
return "mdi:window-open-variant"
|
||||
else:
|
||||
return "mdi:window-open"
|
||||
else:
|
||||
return "mdi:window-closed-variant"
|
||||
|
||||
@@ -169,10 +169,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",
|
||||
event.origin.name if event and event.origin else None,
|
||||
)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
old_state = self._attr_is_on
|
||||
self._attr_is_on = self.my_climate.motion_state == STATE_ON
|
||||
if old_state != self._attr_is_on:
|
||||
@@ -205,10 +202,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",
|
||||
event.origin.name if event and event.origin else None,
|
||||
)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
old_state = self._attr_is_on
|
||||
self._attr_is_on = self.my_climate.presence_state == STATE_ON
|
||||
if old_state != self._attr_is_on:
|
||||
|
||||
@@ -103,6 +103,9 @@ from .const import (
|
||||
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_PRESET,
|
||||
@@ -144,6 +147,7 @@ from .const import (
|
||||
)
|
||||
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
from .open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -216,6 +220,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
_motion_state: bool
|
||||
_presence_state: bool
|
||||
_security_state: bool
|
||||
_window_auto_state: bool
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
@@ -270,6 +275,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._underlying_climate_start_hvac_action_date = None
|
||||
self._underlying_climate_delta_t = 0
|
||||
|
||||
self._window_auto_open_threshold = 0
|
||||
self._window_auto_close_threshold = 0
|
||||
self._window_auto_max_duration = 0
|
||||
self._window_auto_state = False
|
||||
self._window_auto_on = False
|
||||
self._window_auto_algo = None
|
||||
|
||||
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
|
||||
|
||||
self.post_init(entry_infos)
|
||||
@@ -340,6 +352,27 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
|
||||
self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR)
|
||||
self._window_delay_sec = entry_infos.get(CONF_WINDOW_DELAY)
|
||||
|
||||
self._window_auto_open_threshold = entry_infos.get(
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD
|
||||
)
|
||||
self._window_auto_close_threshold = entry_infos.get(
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD
|
||||
)
|
||||
self._window_auto_max_duration = entry_infos.get(CONF_WINDOW_AUTO_MAX_DURATION)
|
||||
self._window_auto_on = (
|
||||
self._window_auto_open_threshold is not None
|
||||
and self._window_auto_open_threshold > 0.0
|
||||
and self._window_auto_close_threshold is not None
|
||||
and self._window_auto_max_duration is not None
|
||||
and self._window_auto_max_duration > 0
|
||||
)
|
||||
self._window_auto_state = False
|
||||
self._window_auto_algo = WindowOpenDetectionAlgorithm(
|
||||
alert_threshold=self._window_auto_open_threshold,
|
||||
end_alert_threshold=self._window_auto_close_threshold,
|
||||
)
|
||||
|
||||
self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR)
|
||||
self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY)
|
||||
self._motion_preset = entry_infos.get(CONF_MOTION_PRESET)
|
||||
@@ -1044,6 +1077,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Get the window_state"""
|
||||
return self._window_state
|
||||
|
||||
@property
|
||||
def window_auto_state(self) -> bool | None:
|
||||
"""Get the window_auto_state"""
|
||||
return STATE_ON if self._window_auto_state else STATE_OFF
|
||||
|
||||
@property
|
||||
def security_state(self) -> bool | None:
|
||||
"""Get the security_state"""
|
||||
@@ -1074,6 +1112,41 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Get the last external temperature datetime"""
|
||||
return self._last_ext_temperature_mesure
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode, e.g., home, away, temp.
|
||||
|
||||
Requires ClimateEntityFeature.PRESET_MODE.
|
||||
"""
|
||||
return self._attr_preset_mode
|
||||
|
||||
@property
|
||||
def preset_modes(self) -> list[str] | None:
|
||||
"""Return a list of available preset modes.
|
||||
|
||||
Requires ClimateEntityFeature.PRESET_MODE.
|
||||
"""
|
||||
return self._attr_preset_modes
|
||||
|
||||
@property
|
||||
def is_over_climate(self) -> bool | None:
|
||||
"""return True is the thermostat is over a climate
|
||||
or False is over switch"""
|
||||
return self._is_over_climate
|
||||
|
||||
@property
|
||||
def last_temperature_slope(self) -> float | None:
|
||||
"""Return the last temperature slope curve if any"""
|
||||
if not self._window_auto_algo:
|
||||
return None
|
||||
else:
|
||||
return self._window_auto_algo.last_slope
|
||||
|
||||
@property
|
||||
def is_window_auto_enabled(self) -> bool:
|
||||
"""True if the Window auto feature is enabled"""
|
||||
return self._window_auto_on
|
||||
|
||||
def turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
if self._is_over_climate and self._underlying_climate:
|
||||
@@ -1102,28 +1175,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode, e.g., home, away, temp.
|
||||
|
||||
Requires ClimateEntityFeature.PRESET_MODE.
|
||||
"""
|
||||
return self._attr_preset_mode
|
||||
|
||||
@property
|
||||
def preset_modes(self) -> list[str] | None:
|
||||
"""Return a list of available preset modes.
|
||||
|
||||
Requires ClimateEntityFeature.PRESET_MODE.
|
||||
"""
|
||||
return self._attr_preset_modes
|
||||
|
||||
@property
|
||||
def is_over_climate(self) -> bool | None:
|
||||
"""return True is the thermostat is over a climate
|
||||
or False is over switch"""
|
||||
return self._is_over_climate
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target hvac mode."""
|
||||
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
|
||||
@@ -1621,6 +1672,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
if self._security_state:
|
||||
await self.check_security()
|
||||
|
||||
# check window_auto
|
||||
await self._async_manage_window_auto()
|
||||
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update temperature from sensor: %s", ex)
|
||||
|
||||
@@ -1815,6 +1869,80 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context
|
||||
)
|
||||
|
||||
async def _async_manage_window_auto(self):
|
||||
"""The management of the window auto feature"""
|
||||
|
||||
async def dearm_window_auto(_):
|
||||
"""Callback that will be called after end of WINDOW_AUTO_MAX_DURATION"""
|
||||
_LOGGER.info("Unset window auto because MAX_DURATION is exceeded")
|
||||
await deactivate_window_auto(auto=True)
|
||||
|
||||
async def deactivate_window_auto(auto=False):
|
||||
"""Deactivation of the Window auto state"""
|
||||
_LOGGER.warning(
|
||||
"%s - End auto detection of open window slope=%.3f", self, slope
|
||||
)
|
||||
# Send an event
|
||||
cause = "max duration expiration" if auto else "end of slope alert"
|
||||
self.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{"type": "end", "cause": cause, "curve_slope": slope},
|
||||
)
|
||||
# Set attributes
|
||||
self._window_auto_state = False
|
||||
await self.restore_hvac_mode()
|
||||
|
||||
if self._window_call_cancel:
|
||||
self._window_call_cancel()
|
||||
self._window_call_cancel = None
|
||||
|
||||
if not self._window_auto_algo:
|
||||
return
|
||||
|
||||
slope = self._window_auto_algo.add_temp_measurement(
|
||||
temperature=self._cur_temp, datetime_measure=self._last_temperature_mesure
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"%s - Window auto is on, check the alert. last slope is %.3f",
|
||||
self,
|
||||
slope if slope is not None else 0.0,
|
||||
)
|
||||
if (
|
||||
self._window_auto_algo.is_window_open_detected()
|
||||
and self._window_auto_state is False
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"%s - Start auto detection of open window slope=%.3f", self, slope
|
||||
)
|
||||
# Send an event
|
||||
self.send_event(
|
||||
EventType.WINDOW_AUTO_EVENT,
|
||||
{"type": "start", "cause": "slope alert", "curve_slope": slope},
|
||||
)
|
||||
# Set attributes
|
||||
self._window_auto_state = True
|
||||
self.save_hvac_mode()
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
# Arm the end trigger
|
||||
if self._window_call_cancel:
|
||||
self._window_call_cancel()
|
||||
self._window_call_cancel = None
|
||||
self._window_call_cancel = async_call_later(
|
||||
self.hass,
|
||||
timedelta(minutes=self._window_auto_max_duration),
|
||||
dearm_window_auto,
|
||||
)
|
||||
|
||||
elif (
|
||||
self._window_auto_algo.is_window_close_detected()
|
||||
and self._window_auto_state is True
|
||||
):
|
||||
await deactivate_window_auto(False)
|
||||
|
||||
# For testing purpose we need to return the inner function
|
||||
return dearm_window_auto
|
||||
|
||||
def save_preset_mode(self):
|
||||
"""Save the current preset mode to be restored later
|
||||
We never save a hidden preset mode
|
||||
|
||||
@@ -58,7 +58,7 @@ class VersatileThermostatBaseEntity(Entity):
|
||||
try:
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
_LOGGER.debug("Device_info is %s", entity.device_info)
|
||||
# _LOGGER.debug("Device_info is %s", entity.device_info)
|
||||
if entity.device_info == self.device_info:
|
||||
_LOGGER.debug("Found %s!", entity)
|
||||
return entity
|
||||
|
||||
@@ -47,6 +47,9 @@ from .const import (
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_WINDOW_DELAY,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_MOTION_DELAY,
|
||||
CONF_MOTION_PRESET,
|
||||
@@ -79,6 +82,7 @@ from .const import (
|
||||
CONF_USE_POWER_FEATURE,
|
||||
CONF_THERMOSTAT_TYPES,
|
||||
UnknownEntity,
|
||||
WindowOpenDetectionMethod,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -167,7 +171,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
is_empty: bool = not bool(infos)
|
||||
# Fix features selection depending to infos
|
||||
self._infos[CONF_USE_WINDOW_FEATURE] = (
|
||||
is_empty or self._infos.get(CONF_WINDOW_SENSOR) is not None
|
||||
is_empty
|
||||
or self._infos.get(CONF_WINDOW_SENSOR) is not None
|
||||
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
|
||||
)
|
||||
self._infos[CONF_USE_MOTION_FEATURE] = (
|
||||
is_empty or self._infos.get(CONF_MOTION_SENSOR) is not None
|
||||
@@ -195,12 +201,11 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
|
||||
),
|
||||
),
|
||||
# vol.In(temp_sensors),
|
||||
vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
|
||||
),
|
||||
), # vol.In(temp_sensors),
|
||||
),
|
||||
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
|
||||
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
|
||||
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
|
||||
@@ -218,7 +223,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
), # vol.In(switches),
|
||||
),
|
||||
vol.Required(
|
||||
CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI
|
||||
): vol.In(
|
||||
@@ -233,7 +238,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
{
|
||||
vol.Required(CONF_CLIMATE): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||
), # vol.In(climates),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -257,8 +262,11 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
), # vol.In(window_sensors),
|
||||
),
|
||||
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int,
|
||||
vol.Optional(CONF_WINDOW_AUTO_OPEN_THRESHOLD): vol.Coerce(float),
|
||||
vol.Optional(CONF_WINDOW_AUTO_CLOSE_THRESHOLD): vol.Coerce(float),
|
||||
vol.Optional(CONF_WINDOW_AUTO_MAX_DURATION): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -268,7 +276,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
), # vol.In(window_sensors),
|
||||
),
|
||||
vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int,
|
||||
vol.Optional(CONF_MOTION_PRESET, default="comfort"): vol.In(
|
||||
CONF_PRESETS_SELECTIONABLE
|
||||
@@ -285,12 +293,12 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
|
||||
),
|
||||
), # vol.In(power_sensors),
|
||||
),
|
||||
vol.Optional(CONF_MAX_POWER_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
|
||||
),
|
||||
), # vol.In(power_sensors),
|
||||
),
|
||||
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
@@ -305,7 +313,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
INPUT_BOOLEAN_DOMAIN,
|
||||
]
|
||||
),
|
||||
), # vol.In(presence_sensors),
|
||||
),
|
||||
}
|
||||
).extend(
|
||||
{
|
||||
@@ -357,6 +365,19 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
)
|
||||
raise UnknownEntity(conf)
|
||||
|
||||
# Check that only one window feature is used
|
||||
ws = data.get(CONF_WINDOW_SENSOR) # pylint: disable=invalid-name
|
||||
waot = data.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD)
|
||||
wact = data.get(CONF_WINDOW_AUTO_CLOSE_THRESHOLD)
|
||||
wamd = data.get(CONF_WINDOW_AUTO_MAX_DURATION)
|
||||
if ws is not None and (
|
||||
waot is not None or wact is not None or wamd is not None
|
||||
):
|
||||
_LOGGER.error(
|
||||
"Only one window detection method should be used. Use window_sensor or auto window open detection but not both"
|
||||
)
|
||||
raise WindowOpenDetectionMethod(CONF_WINDOW_SENSOR)
|
||||
|
||||
def merge_user_input(self, data_schema: vol.Schema, user_input: dict):
|
||||
"""For each schema entry not in user_input, set or remove values in infos"""
|
||||
self._infos.update(user_input)
|
||||
@@ -387,6 +408,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
await self.validate_input(user_input)
|
||||
except UnknownEntity as err:
|
||||
errors[str(err)] = "unknown_entity"
|
||||
except WindowOpenDetectionMethod as err:
|
||||
errors[str(err)] = "window_open_detection_method"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
@@ -61,6 +61,9 @@ 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_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_PRESETS = {
|
||||
p: f"{p}_temp"
|
||||
@@ -97,6 +100,9 @@ ALL_CONF = (
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_WINDOW_DELAY,
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
|
||||
CONF_WINDOW_AUTO_MAX_DURATION,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_MOTION_DELAY,
|
||||
CONF_MOTION_PRESET,
|
||||
@@ -153,7 +159,12 @@ class EventType(Enum):
|
||||
TEMPERATURE_EVENT: str = "versatile_thermostat_temperature_event"
|
||||
HVAC_MODE_EVENT: str = "versatile_thermostat_hvac_mode_event"
|
||||
PRESET_EVENT: str = "versatile_thermostat_preset_event"
|
||||
WINDOW_AUTO_EVENT: str = "versatile_thermostat_window_auto_event"
|
||||
|
||||
|
||||
class UnknownEntity(HomeAssistantError):
|
||||
"""Error to indicate there is an unknown entity_id given."""
|
||||
|
||||
|
||||
class WindowOpenDetectionMethod(HomeAssistantError):
|
||||
"""Error to indicate there is an error in the window open detection method given."""
|
||||
|
||||
102
custom_components/versatile_thermostat/open_window_algorithm.py
Normal file
102
custom_components/versatile_thermostat/open_window_algorithm.py
Normal file
@@ -0,0 +1,102 @@
|
||||
""" This file implements the Open Window by temperature algorithm
|
||||
This algo works the following way:
|
||||
- each time a new temperature is measured
|
||||
- calculate the slope of the temperature curve. For this we calculate the slope(t) = 1/2 slope(t-1) + 1/2 * dTemp / dt
|
||||
- if the slope is lower than a threshold the window opens alert is notified
|
||||
- if the slope regain positive the end of the window open alert is notified
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def add_temp_measurement(
|
||||
self, temperature: float, datetime_measure: datetime
|
||||
) -> float:
|
||||
"""Add a new temperature measurement
|
||||
returns the last slope
|
||||
"""
|
||||
if self._last_datetime is None or self._last_temperature is None:
|
||||
_LOGGER.debug("First initialisation")
|
||||
self._last_datetime = datetime_measure
|
||||
self._last_temperature = temperature
|
||||
return None
|
||||
|
||||
_LOGGER.debug(
|
||||
"We are already initialized slope=%s last_temp=%0.2f",
|
||||
self._last_slope,
|
||||
self._last_temperature,
|
||||
)
|
||||
delta_t = float((datetime_measure - self._last_datetime).total_seconds() / 60.0)
|
||||
if delta_t <= 0:
|
||||
_LOGGER.warning(
|
||||
"Delta t is 0 or < 0 which should be not possible. We stop here the open window detection algorithm"
|
||||
)
|
||||
return None
|
||||
|
||||
delta_temp = float(temperature - self._last_temperature)
|
||||
new_slope = delta_temp / delta_t
|
||||
|
||||
lspe = self._last_slope
|
||||
if self._last_slope is None:
|
||||
self._last_slope = new_slope
|
||||
else:
|
||||
self._last_slope = (0.5 * self._last_slope) + (0.5 * new_slope)
|
||||
|
||||
self._last_datetime = datetime_measure
|
||||
self._last_temperature = temperature
|
||||
|
||||
_LOGGER.debug(
|
||||
"delta_t=%.3f delta_temp=%.3f new_slope=%.3f last_slope=%s slope=%.3f",
|
||||
delta_t,
|
||||
delta_temp,
|
||||
new_slope,
|
||||
lspe,
|
||||
self._last_slope,
|
||||
)
|
||||
return self._last_slope
|
||||
|
||||
def is_window_open_detected(self) -> bool:
|
||||
"""True if the last calculated slope is under (because negative value) the _alert_threshold"""
|
||||
if self._alert_threshold is None:
|
||||
return False
|
||||
|
||||
return (
|
||||
self._last_slope < -self._alert_threshold
|
||||
if self._last_slope is not None
|
||||
else False
|
||||
)
|
||||
|
||||
def is_window_close_detected(self) -> bool:
|
||||
"""True if the last calculated slope is above (cause negative) the _end_alert_threshold"""
|
||||
if self._end_alert_threshold is None:
|
||||
return False
|
||||
|
||||
return (
|
||||
self._last_slope >= self._end_alert_threshold
|
||||
if self._last_slope is not None
|
||||
else False
|
||||
)
|
||||
|
||||
@property
|
||||
def last_slope(self) -> float:
|
||||
"""Return the last calculated slope"""
|
||||
return self._last_slope
|
||||
@@ -1,3 +1,4 @@
|
||||
# -r requirements_dev.txt
|
||||
# aiodiscover
|
||||
ulid_transform
|
||||
pytest-homeassistant-custom-component
|
||||
@@ -46,6 +46,7 @@ async def async_setup_entry(
|
||||
entities = [
|
||||
LastTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
LastExtTemperatureSensor(hass, unique_id, name, entry.data),
|
||||
TemperatureSlopeSensor(hass, unique_id, name, entry.data),
|
||||
]
|
||||
if entry.data.get(CONF_DEVICE_POWER):
|
||||
entities.append(EnergySensor(hass, unique_id, name, entry.data))
|
||||
@@ -72,10 +73,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",
|
||||
event.origin.name if event and event.origin else None,
|
||||
)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
if math.isnan(self.my_climate.total_energy) or math.isinf(
|
||||
self.my_climate.total_energy
|
||||
@@ -130,10 +128,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",
|
||||
event.origin.name if event and event.origin else None,
|
||||
)
|
||||
_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
|
||||
@@ -190,10 +185,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",
|
||||
event.origin.name if event and event.origin else None,
|
||||
)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
on_percent = (
|
||||
float(self.my_climate.proportional_algorithm.on_percent)
|
||||
@@ -245,10 +237,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",
|
||||
event.origin.name if event and event.origin else None,
|
||||
)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
on_time = (
|
||||
float(self.my_climate.proportional_algorithm.on_time_sec)
|
||||
@@ -293,10 +282,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",
|
||||
event.origin.name if event and event.origin else None,
|
||||
)
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
off_time = (
|
||||
float(self.my_climate.proportional_algorithm.off_time_sec)
|
||||
@@ -341,10 +327,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",
|
||||
event.origin.name if event and event.origin else None,
|
||||
)
|
||||
_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_mesure
|
||||
@@ -373,10 +356,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",
|
||||
event.origin.name if event and event.origin else None,
|
||||
)
|
||||
_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_mesure
|
||||
@@ -391,3 +371,56 @@ class LastExtTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@property
|
||||
def device_class(self) -> SensorDeviceClass | None:
|
||||
return SensorDeviceClass.TIMESTAMP
|
||||
|
||||
|
||||
class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Representation of a sensor which exposes the temperature slope curve"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the slope sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Temperature slope"
|
||||
self._attr_unique_id = f"{self._device_name}_temperature_slope"
|
||||
|
||||
@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)
|
||||
|
||||
last_slope = self.my_climate.last_temperature_slope
|
||||
if last_slope is None:
|
||||
return
|
||||
|
||||
if math.isnan(last_slope) or math.isinf(last_slope):
|
||||
raise ValueError(f"Sensor has illegal state {last_slope}")
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = round(last_slope, self.suggested_display_precision)
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
if self._attr_native_value is None or self._attr_native_value == 0:
|
||||
return "mdi:thermometer"
|
||||
elif self._attr_native_value > 0:
|
||||
return "mdi:thermometer-chevron-up"
|
||||
else:
|
||||
return "mdi:thermometer-chevron-down"
|
||||
|
||||
@property
|
||||
def state_class(self) -> SensorStateClass | None:
|
||||
return SensorStateClass.MEASUREMENT
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
if not self.my_climate:
|
||||
return None
|
||||
|
||||
return self.my_climate.temperature_unit + "/min"
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 2
|
||||
|
||||
@@ -49,10 +49,19 @@
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window delay (seconds)"
|
||||
"window_delay": "Window sensor delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||
"window_auto_open_threshold": "Recommended value: 0.5. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -97,7 +106,8 @@
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id"
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -151,11 +161,20 @@
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used.",
|
||||
"title": "[%key:component::versatile_thermostat::config::step::window::title%]",
|
||||
"description": "[%key:component::versatile_thermostat::config::step::window::description%]",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window delay (seconds)"
|
||||
"window_sensor_entity_id": "[%key:component::versatile_thermostat::config::step::window::data::window_sensor_entity_id%]",
|
||||
"window_delay": "[%key:component::versatile_thermostat::config::step::window::data::window_delay%]",
|
||||
"window_auto_open_threshold": "[%key:component::versatile_thermostat::config::step::window::data::window_auto_open_threshold%]",
|
||||
"window_auto_close_threshold": "[%key:component::versatile_thermostat::config::step::window::data::window_auto_close_threshold%]",
|
||||
"window_auto_max_duration": "[%key:component::versatile_thermostat::config::step::window::data::window_auto_max_duration%]"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "[%key:component::versatile_thermostat::config::step::window::data_description::window_sensor_entity_id%]",
|
||||
"window_auto_open_threshold": "[%key:component::versatile_thermostat::config::step::window::data_description::window_auto_open_threshold%]",
|
||||
"window_auto_close_threshold": "[%key:component::versatile_thermostat::config::step::window::data_description::window_auto_close_threshold%]",
|
||||
"window_auto_max_duration": "[%key:component::versatile_thermostat::config::step::window::data_description::window_auto_max_duration%]"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -199,11 +218,12 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id"
|
||||
"unknown": "[%key:component::versatile_thermostat::config::error::unknown%]",
|
||||
"unknown_entity": "[%key:component::versatile_thermostat::config::error::unknown_entity%]",
|
||||
"window_open_detection_method": "[%key:component::versatile_thermostat::config::error::window_open_detection_method%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
"already_configured": "[%key:component::versatile_thermostat::config::abort::already_configured%]"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
""" Test the OpenWindow algorithm """
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from ..open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
|
||||
|
||||
async def test_open_window_algo(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
):
|
||||
"""Tests the Algo"""
|
||||
|
||||
the_algo = WindowOpenDetectionAlgorithm(1.0, 0.0)
|
||||
assert the_algo.last_slope is None
|
||||
|
||||
tz = get_tz(hass)
|
||||
now = datetime.now(tz)
|
||||
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# We need at least 2 measurement
|
||||
assert last_slope is None
|
||||
assert the_algo.last_slope is None
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# No slope because same temperature
|
||||
assert last_slope == 0
|
||||
assert the_algo.last_slope == 0
|
||||
assert the_algo.is_window_close_detected() is True
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=9, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -0.5
|
||||
assert the_algo.last_slope == -0.5
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
# A new temperature with 2 degre less
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=7, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -0.5 / 2.0 - 2.0 / 2.0
|
||||
assert the_algo.last_slope == -1.25
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True
|
||||
|
||||
# A new temperature with 1 degre less
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=6, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -1.25 / 2 - 1.0 / 2.0
|
||||
assert the_algo.last_slope == -1.125
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is True
|
||||
|
||||
# A new temperature with 0 degre less
|
||||
event_timestamp = now - timedelta(minutes=0)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=6, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -1.125 / 2
|
||||
assert the_algo.last_slope == -1.125 / 2
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
# A new temperature with 1 degre more
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=7, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# A slope is calculated
|
||||
assert last_slope == -1.125 / 4 + 0.5
|
||||
assert the_algo.last_slope == 0.21875
|
||||
assert the_algo.is_window_close_detected() is True
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
|
||||
async def test_open_window_algo_wrong(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
):
|
||||
"""Tests the Algo with wrong date"""
|
||||
the_algo = WindowOpenDetectionAlgorithm(1.0, 0.0)
|
||||
assert the_algo.last_slope is None
|
||||
|
||||
tz = get_tz(hass)
|
||||
now = datetime.now(tz)
|
||||
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=10, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# We need at least 2 measurement
|
||||
assert last_slope is None
|
||||
assert the_algo.last_slope is None
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
|
||||
# The next datetime_measurement cannot be in the past
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
last_slope = the_algo.add_temp_measurement(
|
||||
temperature=18, datetime_measure=event_timestamp
|
||||
)
|
||||
|
||||
# No slope because same temperature
|
||||
assert last_slope is None
|
||||
assert the_algo.last_slope is None
|
||||
assert the_algo.is_window_close_detected() is False
|
||||
assert the_algo.is_window_open_detected() is False
|
||||
@@ -49,10 +49,19 @@
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used\nYou can also configure automatic window open detection based on temperature decrease",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window delay (seconds)"
|
||||
"window_delay": "Window sensor delay (seconds)",
|
||||
"window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/min)",
|
||||
"window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/min)",
|
||||
"window_auto_max_duration": "Maximum duration of automatic window open detection (in min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||
"window_auto_open_threshold": "Recommended value: 0.5. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
|
||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -97,7 +106,8 @@
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id"
|
||||
"unknown_entity": "Unknown entity id",
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -151,11 +161,20 @@
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used.",
|
||||
"title": "[%key:component::versatile_thermostat::config::step::window::title%]",
|
||||
"description": "[%key:component::versatile_thermostat::config::step::window::description%]",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window delay (seconds)"
|
||||
"window_sensor_entity_id": "[%key:component::versatile_thermostat::config::step::window::data::window_sensor_entity_id%]",
|
||||
"window_delay": "[%key:component::versatile_thermostat::config::step::window::data::window_delay%]",
|
||||
"window_auto_open_threshold": "[%key:component::versatile_thermostat::config::step::window::data::window_auto_open_threshold%]",
|
||||
"window_auto_close_threshold": "[%key:component::versatile_thermostat::config::step::window::data::window_auto_close_threshold%]",
|
||||
"window_auto_max_duration": "[%key:component::versatile_thermostat::config::step::window::data::window_auto_max_duration%]"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "[%key:component::versatile_thermostat::config::step::window::data_description::window_sensor_entity_id%]",
|
||||
"window_auto_open_threshold": "[%key:component::versatile_thermostat::config::step::window::data_description::window_auto_open_threshold%]",
|
||||
"window_auto_close_threshold": "[%key:component::versatile_thermostat::config::step::window::data_description::window_auto_close_threshold%]",
|
||||
"window_auto_max_duration": "[%key:component::versatile_thermostat::config::step::window::data_description::window_auto_max_duration%]"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -192,18 +211,19 @@
|
||||
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
|
||||
"data": {
|
||||
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
|
||||
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state",
|
||||
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a security off state",
|
||||
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of on_percent the thermostat won't go into security preset",
|
||||
"security_default_on_percent": "The default heating percent value in security preset. Set to 0 to switch off heater in security present"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id"
|
||||
"unknown": "[%key:component::versatile_thermostat::config::error::unknown%]",
|
||||
"unknown_entity": "[%key:component::versatile_thermostat::config::error::unknown_entity%]",
|
||||
"window_open_detection_method": "[%key:component::versatile_thermostat::config::error::window_open_detection_method%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
"already_configured": "[%key:component::versatile_thermostat::config::abort::already_configured%]"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
|
||||
@@ -50,8 +50,17 @@
|
||||
"title": "Gestion d'une ouverture",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Ouverture sensor entity id",
|
||||
"window_delay": "Délai avant extinction (seconds)"
|
||||
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
|
||||
"window_delay": "Délai avant extinction (seconds)",
|
||||
"window_auto_open_threshold": "seuil haut de chute de température pour la détection automatique (en °/min)",
|
||||
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/min)",
|
||||
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
|
||||
"window_auto_open_threshold": "Valeur recommandée: 0.5. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -96,7 +105,8 @@
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Erreur inattendue",
|
||||
"unknown_entity": "entity id inconnu"
|
||||
"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."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
@@ -151,11 +161,20 @@
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Gestion d'une ouverture",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
|
||||
"title": "[%key:component::versatile_thermostat::config::step::window::title%]",
|
||||
"description": "[%key:component::versatile_thermostat::config::step::window::description%]",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Ouverture sensor entity id",
|
||||
"window_delay": "Délai avant extinction (seconds)"
|
||||
"window_sensor_entity_id": "[%key:component::versatile_thermostat::config::step::window::data::window_sensor_entity_id%]",
|
||||
"window_delay": "[%key:component::versatile_thermostat::config::step::window::data::window_delay%]",
|
||||
"window_auto_open_threshold": "[%key:component::versatile_thermostat::config::step::window::data::window_auto_open_threshold%]",
|
||||
"window_auto_close_threshold": "[%key:component::versatile_thermostat::config::step::window::data::window_auto_close_threshold%]",
|
||||
"window_auto_max_duration": "[%key:component::versatile_thermostat::config::step::window::data::window_auto_max_duration%]"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "[%key:component::versatile_thermostat::config::step::window::data_description::window_sensor_entity_id%]",
|
||||
"window_auto_open_threshold": "[%key:component::versatile_thermostat::config::step::window::data_description::window_auto_open_threshold%]",
|
||||
"window_auto_close_threshold": "[%key:component::versatile_thermostat::config::step::window::data_description::window_auto_close_threshold%]",
|
||||
"window_auto_max_duration": "[%key:component::versatile_thermostat::config::step::window::data_description::window_auto_max_duration%]"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -199,11 +218,12 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Erreur inattendue",
|
||||
"unknown_entity": "entity id inconnu"
|
||||
"unknown": "[%key:component::versatile_thermostat::config::error::unknown%]",
|
||||
"unknown_entity": "[%key:component::versatile_thermostat::config::error::unknown_entity%]",
|
||||
"window_open_detection_method": "[%key:component::versatile_thermostat::config::error::window_open_detection_method%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
"already_configured": "[%key:component::versatile_thermostat::config::abort::already_configured%]"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
|
||||
Reference in New Issue
Block a user