From 3b1f4bbd7559035fca694eaef47217c2b8f483bc Mon Sep 17 00:00:00 2001 From: Franco Pellicciotti Date: Tue, 16 Jun 2026 10:33:21 -0400 Subject: [PATCH] Added VT Thermometer to HACS --- .ha_run.lock | 2 +- automations.yaml | 2 +- .../versatile_thermostat/__init__.py | 349 ++ .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 17321 bytes .../auto_start_stop_algorithm.cpython-314.pyc | Bin 0 -> 10980 bytes .../__pycache__/base_entity.cpython-314.pyc | Bin 0 -> 7597 bytes .../__pycache__/base_manager.cpython-314.pyc | Bin 0 -> 5185 bytes .../base_thermostat.cpython-314.pyc | Bin 0 -> 133916 bytes .../__pycache__/binary_sensor.cpython-314.pyc | Bin 0 -> 26054 bytes .../__pycache__/climate.cpython-314.pyc | Bin 0 -> 10994 bytes .../__pycache__/commons.cpython-314.pyc | Bin 0 -> 8324 bytes .../__pycache__/commons_type.cpython-314.pyc | Bin 0 -> 222 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 0 -> 70546 bytes .../__pycache__/config_schema.cpython-314.pyc | Bin 0 -> 28262 bytes .../__pycache__/const.cpython-314.pyc | Bin 0 -> 28618 bytes .../cycle_scheduler.cpython-314.pyc | Bin 0 -> 35531 bytes .../cycle_tick_logic.cpython-314.pyc | Bin 0 -> 5619 bytes .../__pycache__/ema.cpython-314.pyc | Bin 0 -> 4167 bytes ...re_auto_start_stop_manager.cpython-314.pyc | Bin 0 -> 17475 bytes ...ure_central_boiler_manager.cpython-314.pyc | Bin 0 -> 27224 bytes ...ture_central_power_manager.cpython-314.pyc | Bin 0 -> 21780 bytes ..._failure_detection_manager.cpython-314.pyc | Bin 0 -> 28498 bytes .../feature_lock_manager.cpython-314.pyc | Bin 0 -> 10142 bytes .../feature_motion_manager.cpython-314.pyc | Bin 0 -> 17166 bytes .../feature_power_manager.cpython-314.pyc | Bin 0 -> 18362 bytes .../feature_presence_manager.cpython-314.pyc | Bin 0 -> 9668 bytes ...ir_incorrect_state_manager.cpython-314.pyc | Bin 0 -> 8432 bytes .../feature_safety_manager.cpython-314.pyc | Bin 0 -> 17198 bytes ...ature_timed_preset_manager.cpython-314.pyc | Bin 0 -> 16471 bytes .../feature_window_manager.cpython-314.pyc | Bin 0 -> 26199 bytes .../__pycache__/keep_alive.cpython-314.pyc | Bin 0 -> 7780 bytes .../__pycache__/number.cpython-314.pyc | Bin 0 -> 28127 bytes .../open_window_algorithm.cpython-314.pyc | Bin 0 -> 7579 bytes .../opening_degree_algorithm.cpython-314.pyc | Bin 0 -> 2856 bytes .../__pycache__/pi_algorithm.cpython-314.pyc | Bin 0 -> 5235 bytes .../__pycache__/select.cpython-314.pyc | Bin 0 -> 7637 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 0 -> 63235 bytes .../__pycache__/state_manager.cpython-314.pyc | Bin 0 -> 18969 bytes .../__pycache__/switch.cpython-314.pyc | Bin 0 -> 10993 bytes .../thermostat_climate.cpython-314.pyc | Bin 0 -> 63388 bytes .../thermostat_climate_valve.cpython-314.pyc | Bin 0 -> 38056 bytes .../thermostat_prop.cpython-314.pyc | Bin 0 -> 17963 bytes .../thermostat_switch.cpython-314.pyc | Bin 0 -> 10836 bytes .../thermostat_valve.cpython-314.pyc | Bin 0 -> 14220 bytes .../underlying_state_manager.cpython-314.pyc | Bin 0 -> 10947 bytes .../__pycache__/underlyings.cpython-314.pyc | Bin 0 -> 85578 bytes .../vtherm_central_api.cpython-314.pyc | Bin 0 -> 15717 bytes .../vtherm_hvac_mode.cpython-314.pyc | Bin 0 -> 7592 bytes .../__pycache__/vtherm_preset.cpython-314.pyc | Bin 0 -> 2757 bytes .../__pycache__/vtherm_state.cpython-314.pyc | Bin 0 -> 11631 bytes .../auto_start_stop_algorithm.py | 335 ++ .../versatile_thermostat/auto_tpi_manager.py | 3199 +++++++++++++++++ .../versatile_thermostat/base_entity.py | 134 + .../versatile_thermostat/base_manager.py | 71 + .../versatile_thermostat/base_thermostat.py | 2272 ++++++++++++ .../versatile_thermostat/binary_sensor.py | 477 +++ .../versatile_thermostat/climate.py | 231 ++ .../versatile_thermostat/commons.py | 196 + .../versatile_thermostat/commons_type.py | 2 + .../versatile_thermostat/config_flow.py | 1321 +++++++ .../versatile_thermostat/config_schema.py | 588 +++ .../versatile_thermostat/const.py | 691 ++++ .../versatile_thermostat/cycle_scheduler.py | 783 ++++ .../versatile_thermostat/cycle_tick_logic.py | 127 + custom_components/versatile_thermostat/ema.py | 93 + .../feature_auto_start_stop_manager.py | 318 ++ .../feature_central_boiler_manager.py | 468 +++ .../feature_central_power_manager.py | 362 ++ ...ature_heating_failure_detection_manager.py | 615 ++++ .../feature_lock_manager.py | 152 + .../feature_motion_manager.py | 340 ++ .../feature_power_manager.py | 368 ++ .../feature_presence_manager.py | 180 + .../feature_repair_incorrect_state_manager.py | 170 + .../feature_safety_manager.py | 322 ++ .../feature_timed_preset_manager.py | 316 ++ .../feature_window_manager.py | 485 +++ .../versatile_thermostat/icons.json | 18 + .../versatile_thermostat/keep_alive.py | 139 + .../versatile_thermostat/manifest.json | 27 + .../versatile_thermostat/number.py | 560 +++ .../open_window_algorithm.py | 154 + .../opening_degree_algorithm.py | 71 + .../versatile_thermostat/pi_algorithm.py | 124 + .../versatile_thermostat/prop_algo_tpi.py | 227 ++ .../versatile_thermostat/prop_handler_tpi.py | 638 ++++ .../versatile_thermostat/select.py | 126 + .../versatile_thermostat/sensor.py | 1100 ++++++ .../versatile_thermostat/services.yaml | 446 +++ .../versatile_thermostat/state_manager.py | 303 ++ .../versatile_thermostat/strings.json | 1049 ++++++ .../versatile_thermostat/switch.py | 173 + .../thermostat_climate.py | 1299 +++++++ .../thermostat_climate_valve.py | 638 ++++ .../versatile_thermostat/thermostat_prop.py | 344 ++ .../versatile_thermostat/thermostat_switch.py | 226 ++ .../versatile_thermostat/thermostat_valve.py | 313 ++ .../versatile_thermostat/translations/cs.json | 1049 ++++++ .../versatile_thermostat/translations/de.json | 1023 ++++++ .../versatile_thermostat/translations/el.json | 580 +++ .../versatile_thermostat/translations/en.json | 1049 ++++++ .../versatile_thermostat/translations/fr.json | 1051 ++++++ .../versatile_thermostat/translations/it.json | 1051 ++++++ .../versatile_thermostat/translations/pl.json | 940 +++++ .../versatile_thermostat/translations/ru.json | 794 ++++ .../versatile_thermostat/translations/sk.json | 766 ++++ .../translations/zh-Hans.json | 1049 ++++++ .../underlying_state_manager.py | 183 + .../versatile_thermostat/underlyings.py | 1565 ++++++++ .../vtherm_central_api.py | 241 ++ .../versatile_thermostat/vtherm_hvac_mode.py | 164 + .../versatile_thermostat/vtherm_preset.py | 58 + .../versatile_thermostat/vtherm_state.py | 178 + .../versatile-thermostat-ui-card.js | 2912 +++++++++++++++ .../versatile-thermostat-ui-card.js.gz | Bin 0 -> 164896 bytes zigbee2mqtt/coordinator_backup.json | 24 +- 116 files changed, 37607 insertions(+), 14 deletions(-) create mode 100644 custom_components/versatile_thermostat/__init__.py create mode 100644 custom_components/versatile_thermostat/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/auto_start_stop_algorithm.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/base_entity.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/base_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/base_thermostat.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/binary_sensor.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/climate.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/commons.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/commons_type.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/config_flow.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/config_schema.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/const.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/cycle_scheduler.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/cycle_tick_logic.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/ema.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/feature_auto_start_stop_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/feature_central_boiler_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/feature_central_power_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/feature_heating_failure_detection_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/feature_lock_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/feature_motion_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/feature_power_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/feature_presence_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/feature_repair_incorrect_state_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/feature_safety_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/feature_timed_preset_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/feature_window_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/keep_alive.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/number.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/open_window_algorithm.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/opening_degree_algorithm.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/pi_algorithm.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/select.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/sensor.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/state_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/switch.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/thermostat_climate.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/thermostat_climate_valve.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/thermostat_prop.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/thermostat_switch.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/thermostat_valve.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/underlying_state_manager.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/underlyings.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/vtherm_central_api.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/vtherm_hvac_mode.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/vtherm_preset.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/__pycache__/vtherm_state.cpython-314.pyc create mode 100644 custom_components/versatile_thermostat/auto_start_stop_algorithm.py create mode 100644 custom_components/versatile_thermostat/auto_tpi_manager.py create mode 100644 custom_components/versatile_thermostat/base_entity.py create mode 100644 custom_components/versatile_thermostat/base_manager.py create mode 100644 custom_components/versatile_thermostat/base_thermostat.py create mode 100644 custom_components/versatile_thermostat/binary_sensor.py create mode 100644 custom_components/versatile_thermostat/climate.py create mode 100644 custom_components/versatile_thermostat/commons.py create mode 100644 custom_components/versatile_thermostat/commons_type.py create mode 100644 custom_components/versatile_thermostat/config_flow.py create mode 100644 custom_components/versatile_thermostat/config_schema.py create mode 100644 custom_components/versatile_thermostat/const.py create mode 100644 custom_components/versatile_thermostat/cycle_scheduler.py create mode 100644 custom_components/versatile_thermostat/cycle_tick_logic.py create mode 100644 custom_components/versatile_thermostat/ema.py create mode 100644 custom_components/versatile_thermostat/feature_auto_start_stop_manager.py create mode 100644 custom_components/versatile_thermostat/feature_central_boiler_manager.py create mode 100644 custom_components/versatile_thermostat/feature_central_power_manager.py create mode 100644 custom_components/versatile_thermostat/feature_heating_failure_detection_manager.py create mode 100644 custom_components/versatile_thermostat/feature_lock_manager.py create mode 100644 custom_components/versatile_thermostat/feature_motion_manager.py create mode 100644 custom_components/versatile_thermostat/feature_power_manager.py create mode 100644 custom_components/versatile_thermostat/feature_presence_manager.py create mode 100644 custom_components/versatile_thermostat/feature_repair_incorrect_state_manager.py create mode 100644 custom_components/versatile_thermostat/feature_safety_manager.py create mode 100644 custom_components/versatile_thermostat/feature_timed_preset_manager.py create mode 100644 custom_components/versatile_thermostat/feature_window_manager.py create mode 100644 custom_components/versatile_thermostat/icons.json create mode 100644 custom_components/versatile_thermostat/keep_alive.py create mode 100644 custom_components/versatile_thermostat/manifest.json create mode 100644 custom_components/versatile_thermostat/number.py create mode 100644 custom_components/versatile_thermostat/open_window_algorithm.py create mode 100644 custom_components/versatile_thermostat/opening_degree_algorithm.py create mode 100644 custom_components/versatile_thermostat/pi_algorithm.py create mode 100644 custom_components/versatile_thermostat/prop_algo_tpi.py create mode 100644 custom_components/versatile_thermostat/prop_handler_tpi.py create mode 100644 custom_components/versatile_thermostat/select.py create mode 100644 custom_components/versatile_thermostat/sensor.py create mode 100644 custom_components/versatile_thermostat/services.yaml create mode 100644 custom_components/versatile_thermostat/state_manager.py create mode 100644 custom_components/versatile_thermostat/strings.json create mode 100644 custom_components/versatile_thermostat/switch.py create mode 100644 custom_components/versatile_thermostat/thermostat_climate.py create mode 100644 custom_components/versatile_thermostat/thermostat_climate_valve.py create mode 100644 custom_components/versatile_thermostat/thermostat_prop.py create mode 100644 custom_components/versatile_thermostat/thermostat_switch.py create mode 100644 custom_components/versatile_thermostat/thermostat_valve.py create mode 100644 custom_components/versatile_thermostat/translations/cs.json create mode 100644 custom_components/versatile_thermostat/translations/de.json create mode 100644 custom_components/versatile_thermostat/translations/el.json create mode 100644 custom_components/versatile_thermostat/translations/en.json create mode 100644 custom_components/versatile_thermostat/translations/fr.json create mode 100644 custom_components/versatile_thermostat/translations/it.json create mode 100644 custom_components/versatile_thermostat/translations/pl.json create mode 100644 custom_components/versatile_thermostat/translations/ru.json create mode 100644 custom_components/versatile_thermostat/translations/sk.json create mode 100644 custom_components/versatile_thermostat/translations/zh-Hans.json create mode 100644 custom_components/versatile_thermostat/underlying_state_manager.py create mode 100644 custom_components/versatile_thermostat/underlyings.py create mode 100644 custom_components/versatile_thermostat/vtherm_central_api.py create mode 100644 custom_components/versatile_thermostat/vtherm_hvac_mode.py create mode 100644 custom_components/versatile_thermostat/vtherm_preset.py create mode 100644 custom_components/versatile_thermostat/vtherm_state.py create mode 100644 www/community/versatile-thermostat-ui-card/versatile-thermostat-ui-card.js create mode 100644 www/community/versatile-thermostat-ui-card/versatile-thermostat-ui-card.js.gz diff --git a/.ha_run.lock b/.ha_run.lock index 3aa1f83..54d43ec 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 71, "version": 1, "ha_version": "2026.6.3", "start_ts": 1781531265.230539} \ No newline at end of file +{"pid": 71, "version": 1, "ha_version": "2026.6.3", "start_ts": 1781553089.670326} \ No newline at end of file diff --git a/automations.yaml b/automations.yaml index c21b676..df79d91 100644 --- a/automations.yaml +++ b/automations.yaml @@ -288,7 +288,7 @@ conditions: - condition: time after: '10:00:00' - before: '21:00:00' + before: '20:00:00' - condition: state entity_id: light.playroom_light state: 'off' diff --git a/custom_components/versatile_thermostat/__init__.py b/custom_components/versatile_thermostat/__init__.py new file mode 100644 index 0000000..7312294 --- /dev/null +++ b/custom_components/versatile_thermostat/__init__.py @@ -0,0 +1,349 @@ +"""The Versatile Thermostat integration.""" + +from __future__ import annotations + +from typing import Dict + +import asyncio +import logging +from vtherm_api.log_collector import get_vtherm_logger +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +from homeassistant.const import SERVICE_RELOAD, EVENT_HOMEASSISTANT_STARTED + +from homeassistant.config_entries import ConfigEntry, ConfigType +from homeassistant.core import HomeAssistant, CoreState, callback +from homeassistant.helpers.service import async_register_admin_service +from homeassistant.const import UnitOfTemperature + +from vtherm_api.log_collector import VThermLogHandler, async_export_logs, async_register_log_download_endpoint, DEFAULT_MAX_AGE_HOURS +from .base_thermostat import BaseThermostat + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import + +from .vtherm_central_api import VersatileThermostatAPI + +_LOGGER = get_vtherm_logger(__name__) + +SELF_REGULATION_PARAM_SCHEMA = { + vol.Required("kp"): vol.Coerce(float), + vol.Required("ki"): vol.Coerce(float), + vol.Required("k_ext"): vol.Coerce(float), + vol.Required("offset_max"): vol.Coerce(float), + vol.Required("accumulated_error_threshold"): vol.Coerce(float), + "overheat_protection": vol.Coerce(bool), +} + +EMA_PARAM_SCHEMA = { + vol.Required("max_alpha"): vol.Coerce(float), + vol.Required("halflife_sec"): vol.Coerce(float), + vol.Required("precision"): cv.positive_int, +} + +SAFETY_MODE_PARAM_SCHEMA = { + vol.Required("check_outdoor_sensor"): bool, +} + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: 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), + vol.Optional(CONF_LOG_BUFFER_MAX_AGE_HOURS, default=DEFAULT_MAX_AGE_HOURS): cv.positive_int, + } + ), + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup( + hass: HomeAssistant, config: ConfigType +): # pylint: disable=unused-argument + """Initialisation de l'intégration""" + _LOGGER.info( + "Initializing %s integration with config: %s", + DOMAIN, + config.get(DOMAIN), + ) + + async def _handle_reload(_): + """The reload callback""" + await reload_all_vtherm(hass) + + hass.data.setdefault(DOMAIN, {}) + + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + # L'argument config contient votre fichier configuration.yaml + vtherm_config = config.get(DOMAIN) + if vtherm_config is not None: + api.set_global_config(vtherm_config) + else: + _LOGGER.info("No global config from configuration.yaml available") + + # Listen HA starts to initialize all links between + @callback + async def _async_startup_internal(*_): + _LOGGER.info( + "VersatileThermostat - HA is started, initialize all links between VTherm entities" + ) + await api.init_vtherm_links() + await api.notify_central_mode_change() + + if hass.state == CoreState.running: + await _async_startup_internal() + else: + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_startup_internal) + + async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + _handle_reload, + ) + + # Initialize log collector + # Note: Single shared instance stored in hass.data[DOMAIN] is used for all VTherm loggers. + # This ensures that all log records are centralized in one ring buffer, regardless of reload. + if "log_handler" not in hass.data[DOMAIN]: + max_age_hours = ( + vtherm_config.get(CONF_LOG_BUFFER_MAX_AGE_HOURS, DEFAULT_MAX_AGE_HOURS) + if vtherm_config is not None + else DEFAULT_MAX_AGE_HOURS + ) + log_handler = VThermLogHandler(max_age_hours=max_age_hours) + hass.data[DOMAIN]["log_handler"] = log_handler + _LOGGER.info("VTherm log collector initialized (buffer: %d h)", max_age_hours) + + # Register the HTTP endpoint for log downloads + # This provides a /api/versatile_thermostat/logs/ endpoint + await async_register_log_download_endpoint(hass) + + return True + + +async def reload_all_vtherm(hass): + """Handle reload service call.""" + _LOGGER.info("Service %s.reload called: reloading integration", DOMAIN) + + current_entries = hass.config_entries.async_entries(DOMAIN) + + reload_tasks = [ + hass.config_entries.async_reload(entry.entry_id) for entry in current_entries + ] + + await asyncio.gather(*reload_tasks) + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + if api: + await api.central_boiler_manager.reload_central_boiler_entities_list() + await api.init_vtherm_links() + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Versatile Thermostat from a config entry.""" + + name = entry.data.get(CONF_NAME) + _LOGGER.debug( + "%s - Calling async_setup_entry entry: entry_id='%s', value='%s'", + name, + entry.entry_id, + entry.data, + ) + + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + + api.add_entry(entry) + + entry.async_on_unload(entry.add_update_listener(update_listener)) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + if hass.state == CoreState.running: + 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 a component has set this flag, skip the reload (data is still persisted to disk). + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + if api and api.skip_reload_on_config_update: + _LOGGER.debug( + "update_listener: skipping reload for entry '%s' (skip_reload_on_config_update=True)", + entry.entry_id, + ) + return + + if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG: + await reload_all_vtherm(hass) + else: + await hass.config_entries.async_reload(entry.entry_id) + # Reload the central boiler list of entities + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + if api: + await api.central_boiler_manager.reload_central_boiler_entities_list() + await api.init_vtherm_links(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + if api: + if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG: + api.reset_central_config() + api.remove_entry(entry) + await api.central_boiler_manager.reload_central_boiler_entities_list() + + return unload_ok + + +# Example migration function +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Migrate old entry.""" + _LOGGER.debug( + "Migrating from version %s/%s", config_entry.version, config_entry.minor_version + ) + + def calculate_version(major, minor): + return major * 100 + minor + + current_version = calculate_version(CONFIG_VERSION, CONFIG_MINOR_VERSION) + version = calculate_version(config_entry.version, config_entry.minor_version) + + if version != current_version: + _LOGGER.debug( + "Migration to %s/%s is needed", CONFIG_VERSION, CONFIG_MINOR_VERSION + ) + new = {**config_entry.data} + + thermostat_type = config_entry.data.get(CONF_THERMOSTAT_TYPE) + + # Unit test with no version comes with 101 (version=1 and minor_version=1) + if version <= 200: + + # Migration of central config thermostat to add new features flags + 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 + new[CONF_USE_PRESENCE_FEATURE] = new.get(CONF_PRESENCE_SENSOR, None) is not None + + new[CONF_USE_CENTRAL_BOILER_FEATURE] = new.get("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) + + # Migration 2.0 to 2.1 -> rename security parameters into safety + for key in [ + "security_delay_min", + "security_min_on_percent", + "security_default_on_percent", + ]: + new_key = key.replace("security_", "safety_") + old_value = config_entry.data.get(key, None) + if old_value is not None: + new[new_key] = old_value + new.pop(key, None) + + # Version 201 (add auto TPI parameters). Cumul with previous migration if needed + if version <= 201: + # Migration 2.1 to 2.2 -> add auto TPI parameters with default values + new[CONF_AUTO_TPI_MODE] = False + new[CONF_AUTO_TPI_CALCULATION_METHOD] = AUTO_TPI_METHOD_AVG + new[CONF_AUTO_TPI_EMA_ALPHA] = 0.15 + new[CONF_AUTO_TPI_AVG_INITIAL_WEIGHT] = 1 + new[CONF_AUTO_TPI_EMA_DECAY_RATE] = 0.08 + new[CONF_AUTO_TPI_HEATER_HEATING_TIME] = 5 + new[CONF_AUTO_TPI_HEATER_COOLING_TIME] = 5 + new[CONF_AUTO_TPI_HEATING_POWER] = 1.0 + new[CONF_AUTO_TPI_COOLING_POWER] = 1.0 + + # migrate CONF_OFFSET_CALIBRATION_LIST if present into CONF_SYNC_DEVICE_INTERNAL_TEMP_LIST + offset_calib_list = config_entry.data.get(CONF_OFFSET_CALIBRATION_LIST, None) + if offset_calib_list is not None and len(offset_calib_list) > 0: + sync_device_internal_temp_list = [] + for offset in offset_calib_list: + if offset is not None: + sync_device_internal_temp_list.append(offset) + new[CONF_SYNC_ENTITY_LIST] = sync_device_internal_temp_list + new.pop(CONF_OFFSET_CALIBRATION_LIST, None) + new[CONF_SYNC_WITH_CALIBRATION] = True + new[CONF_SYNC_DEVICE_INTERNAL_TEMP] = True + else: + new[CONF_SYNC_WITH_CALIBRATION] = False + new[CONF_SYNC_DEVICE_INTERNAL_TEMP] = False + + if version <= 202: + _LOGGER.info("Cleaning up obsolete Auto TPI keys") + new.pop("auto_tpi_max_coef_int", None) + new.pop("auto_tpi_enable_update_config", None) + new.pop("auto_tpi_enable_notification", None) + new.pop("auto_tpi_keep_ext_learning", None) + new.pop("auto_tpi_continuous_learning", None) + + # Update the config entry with migrated data + hass.config_entries.async_update_entry( + config_entry, + data=new, + version=CONFIG_VERSION, + minor_version=CONFIG_MINOR_VERSION + ) + + _LOGGER.info("Migration to version %s.%s successful", CONFIG_VERSION, CONFIG_MINOR_VERSION) + else: + _LOGGER.info("No migration needed") + + return True diff --git a/custom_components/versatile_thermostat/__pycache__/__init__.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a06376432f2270b722de960723358470800a9372 GIT binary patch literal 17321 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 aOqSm7&1}0VO_~oKi!3wA91xmNM)+Ud)^(u( literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/base_entity.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/base_entity.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e989dc4f5b637a3dfddada6d53fd401bde4da7fc GIT binary patch literal 7597 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/base_manager.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/base_manager.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56d7a85472090985380de686a06501e440385f01 GIT binary patch literal 5185 zcmbtY-ES1v6~D7PyE|U5*B@ZahmFS{BwYx0+W?IUN-@Natq_MSj!~g79q*34Tg}dF z?_48!pH@kwQYmViJhbqX@>bsRn*Rcn2*^lLh)PuD&9GISzEu64JF_3Q*BGTE`_A2S z&)hlpcYgPrbLUvPE6H%(y8Pqce5Nt>5BgER)UZ>#flifWnPAshR+&>~BO)?P{m5K& zRu$@OOvL2x=v;g@Arj~-S#?f3Nm@!aHrFwm6v|CX`v%Zt-~*Q{HzraZ3*HAg_Lcf}IM4MC3@ zw!4B^AykguRs7_6*I6#EykQD67Xl)0(eOv@zv!62=Nc0VFp6C7714dn1yVxI?q6iD zD%t1&v$d>>w#QaPOv++XhBezgBA!hQFp-dQP0Bl@T+1dQ?~w9NDNjjxQcAj{BrPSK z*HTi}EoD6#Irm>#vfaqOhqAwePmk#)S z=ir#oFQHp1gHwUKa<=Jt(n+Rm>&t=j=5yrykN_->J^@;L1=*)6_i5@K+#4fqjMwg}r#GH;U%_D%)VO*ukgN-1gS#cK5ivedbAF4O%Qm3#ngZ zF`!A+me8rP%RNqgge@vBhy8pPfw!n!B<(HN+sZ<8kvWZ#2|(`DIn6HXPUCG^1u#b{ z`{$gZ(A{O-th+Gu^L!@eC&InMyYU)7W)S{`~S(Jn-h)pU$Jk9Lz11D=AK-w8tSH5j+-dOQJ1Cc=Ah z<9%pCD#;~qGLHA7IYcA$OS(D|8-L2s+_@refmsblKr1vWRU?C|>{s<&!cO2fT?Hkh zR3^zGMbj=;EFGlOO(A%3X;oMrd5I*O_akE2o_SX^!l{*?=G?`u;DeZw>u99)bq92m z!E)&Xf3S@Q8l!S?op`#+o}@=UPmkTx#+pz=E-zZs2SnJz=!~-A9oBU+TBNldxg<<3 z^nf@;XGJb&)yZ4jUZbCIK~b#nQOtE$k@kB8Qb?U{M0GAg+CkkPZDaoyonbug5f{H} zUB`bUDc|qxQhqpZ?nVOM#JPp6<-iTAr{QG5p<2k3CBDJWP$;PmMfG9l4)6 z@f@M&A*G(s{2M*O2_^PEB zDKO>=rn6${1@nfbFIkqOm(091bC+#fW)}}8G{8b(`FfjGZqhhPFrhw^?EI#CZ^+AN zY%?!aXpr-2<7l22&$5whbdoZ+8gNrxUUhPulJ9&*;WWkW^qhc}N1HzkYR4#=9lJBM zPPb1_$Y0xI8_kXXRP78pJF&DK0~eKgxj2l{N~WaaQ~A7ZhD1XhC<2~Q)R2ivW}n7z zgLrYXT(+D%Rwx7CYqSmuPxZi;GV}Rn&T5y&TNNY>L7;xDr5T>wkKh|h)Q_Xw%7Ea z)Wk6+FVooe5K;6{fYQ-}blQf9O&TX*UX3cMD*IQQb&qaM#Pww~=%+*Xg`h-4=z;F; zV4jASH-ILL2r~-7IHP=&3+_$7LPuT=_MOAeouWY_X9eYK+nhC4XRfw@!iUPw%D(p; zy63l)L0HWJylgApLFGEoCJ@skKgSauvAxRHjjQ zcu|nwh8HDDza3sw;{O7JvYL{3_3RCBI?%N1ha>cY8+)28Xu&5axoLtF-i0NBvs}!d+0z#RK|fSX&5CpziLw-XV%}YnszwSZ5Yc% z?g_hyW3P+hgkj{}oMG@Gns%I;A5rrYYAEDz+VuP^8ec1Ow`_6L<uycWAU9 z=WG-^#hlSRsNe;?Ehq7uW!r`U!pV~qQRHlJkfLu?oEkc3d4ECk{qMozFV&YeR2-I{ z(TRB@4aZXLG5$^;5sEk?BsZQBLd}G6uouot!trd zMrjCVl!o+RLwK+u3!eTrJuQ784V}+)^Y>t<9~Wz796{u#2*0m@&raAL!Dpw595Spd zwt{aI`@`=~X1O?lZ%~N-whcbHoX2RU7_xyU{1m=Ql+ZIUvBjw-^t*yQVR*TMRWkXz zBuf|<-g)!Q>8!Ly=Ig+?;B0*+@c--l*U%-PJt||?ql%(@$@&3>LdQcY~cPbuRyhDOf!Kd9$fMgRZ+ literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/base_thermostat.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/base_thermostat.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b85f3c68504b9a902270e5ff64dcabdefa7c6951 GIT binary patch literal 133916 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%0D_i&a~5*avIKWDV|Rigg>WjFAW(nmXv*= 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/binary_sensor.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/binary_sensor.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d2a0dd9b947fe0bcfc5a3806985d52c17a55b535 GIT binary patch literal 26054 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/commons.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/commons.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51567fbe33cd7098b416e0736dea55869b5ba125 GIT binary patch literal 8324 zcmbt3ZEPDycC-8zMN-taWlPpbjz!T?NLyBHN0yx^wqi-PWQ8&4L(ev`B3II0$R)kI zlr1(!nce|W(z{xRo^cHBg6KyBxIj_(PXQMw(DZ+SN+;#S+d8O=rYPpriWKq*{3`lG*VBji8v!z@;#vAGP5Jc$sM+#nHd zj8i#yTgEJ-R@K_6_t{h%^jXL3qYl-<`fOv)QJ3msZTncws9UXtHWzV>c}Bgechsl) zI>~iw7r90lSvv+r&4K2q46ZID0DK&Y4x*$8k<+=1UVH;re|jb89KeAFlbm& zL`CH#6*_9qrKK5hc0^L;1^BEJBvn)wVzMv`Akbye+CC8Ec`+{NfLWPW#k4dc%V}A2 zoWDFVJT@6eh76{N1KKFc%^|4rBuZ{rPW^qz^|M z#nh7*U@G82OdLN12qWpNl%PD7vZ-l7rfQl3uT&CvSx{7ZoA%zC6XSDKnM-Gr2|6v% zgb)`~e3DLQRSLYrz+^J$9=Xk@GD%_3O`%BX)U+%dqi5*xP=7^FKlBWQPO_fc&^{VE z?ymS9<3Ty3gop!WBrB$-V+JL?2F^ZuJGjFT#Qc&W{cH#ulaWP9H87PY3sQQBUe2nS ztU3tv9)Brhe--3F=!6@Uip?6d$hbeqC}0KGw5La|gO}#tPL$jP-Aqefhen>1_7jhH z@#at8%wJl3^HHsTseXwsblnr~9ayXFSgv1w|EuP2Y6B~_fLssoRR)9efNqe@*BGYV z4JM4XZ;@*NRQ5mxq}vi=9Lz!X!KZEujAo|CtU820TA)H@PH8Ot@~hm~l#7q$VAR|2f7om?wx zdBhqe9ul$jTg}mPT-0)d^zAn>5R$P%4_DP=i`sUi!ER#7*ehr#_W}*KcA&w*XmD21 zz)d-~C@@{_A04w*L>7eC?rAix|6(gKwX|P>4 z@vk@{!ev}h=Z-QhpT-$i1r}Es3)7g{Fw@#Ct#(z*sSHg{4Z=>p?*-^8IaNcyU=jYP z6V|JK59`$s-A2c55{@(ibQK-DLN|RM+}{Utn5$gtiSCNMVDFqag-Mt*

(XygD?y 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/config_flow.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/config_flow.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dbc2a961e547e53b1e2ab18a3071bd6fa2be1df0 GIT binary patch literal 70546 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 M9a(+tBMFKGpaV!fEb0V-pa_`+m;)%= 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 O3&s2NQKlN-KmTuNe^+1t literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/const.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/const.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96f398ea6847bfff94fa55449274300d6d2da190 GIT binary patch literal 28618 zcmdUX349yJb@u`UNPyyf-vm#II<3R9d`b`r3b06k9)Of(3ql|gBoUDWy%_3p4mxRL zr)d%=X&a}f)22<-=4jKjO?t*@n&vJjr4hWb8|F*Wv|p1Hv@FX>&;Og*T^yujeNOuO ze&vrdnD^eCZ{BFj4>+R!x-hSTi z-OM+8x9}~@TqA zvXGGi_JWbl{sQ~DEM%mBeS?wC{sOz4g^bkBV(t!OrcPVHmpXo!)$>8tz=v2P_pl~D z%$oTT*22B4mHSv5zn``9qpX9EuueY8T-?vPcz|{DG1kK$V7;8NJ|1NKJj6Ehakhm| zumOIIZRHQLZTvXf&L3hs_`_@`Kf!kKFuRSPWV?BU?cuLsdwG=Y}ewy9K=h#6$&kpeg=H>}@m@l$H{slt0S+{BlD~^h z@prQre-E4HzrberFS1$wODxXc%U;W$VyF50*c|_5HqU>BE%0Au3I1zrk^ee-g#QLp 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/cycle_scheduler.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/cycle_scheduler.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c42bef16d74b79a2baf7fac40101bb7aa5e4cab GIT binary patch literal 35531 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 U52gnfC8S{wrr^ULPSs4~zjRi|+yDRo literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/ema.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/ema.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ec7f698a40eeae70f6a514b5009cbfd3dacb33f6 GIT binary patch literal 4167 zcma)9U2Gf25#HnR_%D6Jp`x?7>!dD$WIF-k9L6~$y1uAKKG%3WLXe(pe;n8e7JP{N}`RBe`3ci-g;wi1sdxlLu|4_GQyl-3s8IJJhP%L z_Sbk`+Y3B#PMVc$Y1U`^`pG46fL!vQ6lzlJzzVU0aWX(M-X4;XPI~J6P(~OcNnb@- zTvKVvvbCaOt9d$aUeXlGlWcr-$f)j0iNQ3%100a9!X@$g_Q% z=G9pk<#tT%; zNXVIx&}QGD4V}yK1#aR5OH9pa7Ff;+7M1I=qLkWJHx-+qt2v@(*kBDGm==7%X_IAs-@KNyW!b{L2G^Y*Oyo?Xpsh~i%9d>w<(yfp z>C2h`SFylDbXDf|H7#4QC)A=cUb^8#WLYr`6F90YJ0s6i@_6gCKG@MZ1LWIv@{~lS z;m1h6?&{f8A*~nGJbaP8kD;+n zmZ;&5kfq~>Ya?*0EHUb@gplQigmj=Wz0nu|3#J=$8*JjfK9D52+@+Y&khOHq@HhBr zqam5UNl)d}B6yc;QmUcrsWe}PCx`D)GpMD4HS(%O%>q@Zfris*?nX{m7m`6ID7$N+ z+3<&moGUX19xj7z7%IaN4fC24mBG7hJ`XHcEICrmr$TbmfkJZAgM5zHQuP80f&_*h z&B90`NFbCE3vW5f3+sfY6Od)i(7>0Rffv}nHig^%6s%e&yB%a8dHX=MZve>9%Qw9b z0(}pYuYQ*Jed6={m&O;yRx*XmL^a-jJ5)V5bUO;5;)rg-&dX(PS( zmk1xJ-3{;J7Ia|fNA@B>Ar=cNywwEBdJ5o$39{_YBk9&vbuLB;H<$Lq+95Qcx3Q$r zC`>#ft3@;Jq>QIk7Q~50M1dI9kh#D7A&6u9MW!))ne~#!2l`AoS?`y`thkJCmzOE< z?|kxmoLJ0~)^#WArvy#xtA!!5A}r%O;w2q(Tg`faw2b2`d!`_MX+%|VtPxcS?ugIs znKIULXLntzPDZy$m(opJ zxA@zBJ_4X7>&bev;CK7^Synn(U)4{RUvf8m&RmjCxHFoydtXEtm3PnAfuG8AkD*?p z5P)cbnMDc-2f8gU*=^@30HnsF4rO=%tsx~?3&3#!a0%5QWdVjNDGZPcKp2ZEfFl7T zKw{GnerXlJnUI-6mGu#e9;4?uf@nZdz+Lmxr`$a;l2C-7jeWVeBCr(OWH-j?;u<7S z2sBm=&l(g^YCU7ZaUuRNyIi7|%g|mk%Q|jtQ^2>DX4#d4`~+`l%&Hv^6E25-^V5|# z_!#ah=hd8YgF+v3wP{id^07{joCa2MoW5l;fWLfQi`M>KQ~jf}l^-<(@o0Dkx*V*X z85uuOXuW8S(UQh%3@``gblyou9l!kJ3o|pRjN`wiFawY*I|y4jQeM4WUS()`r_BaP zrpq-LVL2W1a|`m~+bwte;C`X4%NI z!_ZFpcsgPbHweS@;)I$?aU^~f$6r6Biw-EIwanG#{$i+j2WRxT`RhCLc=VrxECo0r zL?S)+!-qdj-XDDJVWjiD3%4%pdPw_4Pc_!{L=xK$>;_0z|3>8Sqi}R5oY)K}?uUnQ z;{2`oYHX}!*1*_<$f4>GP+INm+36hK>>S>R3_t4b-x-|T9Gu+lp4^G2Hsh(S_{_~v zH5`A?d-Os0XhT*SWL0AaAI1is1SQxPz-Vt%O04}iZ+)GEee)9ImZ-1Km?WMav z+dlNeKMwzC{HyWpL$h05bK9}Go6^5xop(~7oc-wRR_v9VQnjOJyJO^T``w@Zq3`qj z&g9JIlwH1lQn6Oa-4|Dvqu^^vG77w{438vJ9KFbsetZ{}o~4WOh^ zBnUB_I06~Kxj9*}ZKlDqR4tg^-ttqDQ*~XI0R(X{hDhaH4T~Z0WJnMJS}60|UxI`D z)jRXl3x5Rv!QY6dgMi9*5t=>q0Z{laq6L0+zAzbP)3D6(<3B3A9H+Pb&#IKPap1vk zkFKjZ2rLY9G&_NW>jD!X!=Dz0uE}0Sf=9VWYSL>+_;gJ@Z?G8{!joH=hj&Fm5FUF4 uVPMZkgy`SNHiD*Nh;%zDJO9ss96(WUGGw literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/feature_auto_start_stop_manager.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/feature_auto_start_stop_manager.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0f7505b8e7714e0c7a3d8918f268389dd26d953 GIT binary patch literal 17475 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/feature_central_power_manager.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/feature_central_power_manager.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad41fd6bd695c8419de41aaa181e00c7905881de GIT binary patch literal 21780 zcmdUXYit`=mSztL}+8fakR>E4}WdV4}za>x|iXg4~&*dFW; zJ4(0PzyiD9x%J>fl;osmda(ua)~)-hd(S=R+;h%7xAv5l6mz()o$0*r%wdlEH9hE; zRg0|s6CzhR9~a}E<$U}wALDU14V#9{F>^honTIVy)|i#WEyD#vg|R{ww+`Eeieg19 zUNBrdv@N!c#S4e+LnX12q0(6CkR#?`X|`eKP+6>Os619aE+; zhbm*0L!MX_o_XK4;pzh>7PAl64AsVJS-fO;$536Yj>Sud>xYDxfVexh^PG7ncdntE zSH~1WlMd-^9x~dUW~+KAt21)E(OBEc=VDe2?Jsv zwkV0hNFWlJ!}DxZ5~f5+4#YxXQJ6e0O7l@U7KjO>US27i6J!31F-r7@qjM;aM00QC z3U0+S^9%l1)c?FlPerlNyciV2u>d_xO!iLp`^N?byk@0jC^|3p%5n&;Mq-M!|04RM z*k%IZ@N{72d6cLN$X6mWespr?c|W~d^v|3RMCL@l$jVl*Dif4(oTUf*0|AlTva#9GXYAnxho-jg9mU)8P2sRN;~o!u!~ZXo&A& zV8^48+0Y!l0|m;D1!PeJng+8O4YAlwgFUzQKQT7}F2G;q&T%mw*k*dRC1&2vMR^|| zvrO?_yw6n0g>6$DVrGhkoiQs*x6qT%iqIJ=@D)^YvBGmUmQ%=bY?RZjy){;(%mQuk| zwzHJ-b1s%v$a4s9Ew6^<)n?^w*Yb9-yt=HsN-eLRzkeo-qQTHi%GJ1*)B!Ov))F@z3n5PprH_!o$u__ZOmFI-XVet#em zi82EAE3F#?s6Dfw7rl{d5d7gPcb{`uYwuC;*15ZM`;nPFts$G6(MgRSh`Hn0zV?rZ z5Y*06D!V4kQ`~dN_wiF8Ih{Gyb7!0p8acbp2oa+-AIe8)7TASaT!dOrKNVy>5rEmn zO4_oXin5;A(nKj8nA6C%tUSB%L~k!Ko``{4mOR6eP)vx<3IRcbx`tY#ic153Cc+JP zOBJiUxFAZ3&F>GPuHUa%{R}&{>HHh$p_KYVGUl1s8`glJQmIX%EJoz0^ zr3UkQd85oy|IDH!p|Dy&DL0;tO3F5^a!j0GQ0o0Nfyj&)#w|RvNW9uFpBIC{P-Kph z!pcr!`%+8{>Ya@Ni^5tH)2v4>@OU|vt`|frN`bmucrA)W7Q?er72qgUBT$Moln1)g z)V8#=T$OfEZWHA;Zi<^K02y9JQ5typg{|b(!i3Fp&3!EvZ$7kYJDh6T_1)rBP4lYF zlWOuVTUKo~sjeQnH>G+G(|uQ}ty4|!?52Bt%Db2Do>WH{-P=;f`{~|(r?HjpLTY=> zvgO}wlLWjoi$f6B61J{U&So5g99WmiTW87a`KgODCIUVXrV6HCEK@k|7xq>|!L?u8Ftmazvu9a48fH3)7+y6azD{&_(cz z;OM~QQa@`@6^1iPqaiF^!t7!=e8uRE7!(4LV7`{TPKnlQrFu3L2^vdcR^OCD|H-kz z!G51&Au3eN5T=!~<71-({_(Lh{XYLh|LDY+PufN8RIv1s-lwwjhzaR+eEk#slWMlq zhIEOhN7_xn9xAq-HF0{PUu$BZzjyMquU~3Mrq?X(qk=#PPASz_aJ9QK(PvAGz_@`@ zw1Cx~tszR&CS1}e#h6AMEnMa9RP9Jsc@tIMWL00Hs&A!gV7c^{jvcq{uGdb!dNN)$ z7Z1&^+9RLa%kQ-A|G`V|ytLXgw9-0E%&#f6=aK((^u432hsRg;oT8LnshXC*Ir`So z)jda6Y96B$&z-tPy4R%Y8q|A33*GDQ*ehQfdv$EJcK`MMr2WuM`=ON6opg5M7k75D zcw5rh7I(I>xR7)Tai?$xwfk1>^{E3r%YC2O_kW>x^SSu+>}U45G#|#`*j0$|6_PoQ zL3V54>Nn(gI^E0xvY;DqWzZ=>Ip@;3U_Ip~dz!FeW%@Io2uUScBes?{ zo<}K}+GEm&)!KNEL-2k@qUYqTeIx8)y}&{3zd|HvF-Y*Nu1Ilb4bfzh3IF4UD1lnq zl~<@R=_H{OZV($LY*r5ca7Hn4!!3 zRr5(}G1!jJ+{hJjCkq%6S(Yk>utvdlqtcCv%yRoujZd5rLl^aQm7WA3X-pK9qKJ4& zm7Pbug%Xx`PCAG{u|TX2NW{~jP*72}dMG85I4gR*mI{edVWqSoX=_-uH9U|>mV3GPz%h=$(ZlyXZn<%=5b=LLYLQOh1=0wDt*Jvs zsWf$%i8>Ixe>!n^{MLa}>|qn?_&sW?3)KIQ)B$=R7Ls#4G(A*h(F0Uc!JvtHohB$X zbDh!XLK?$MN4*7Uijc;U!U#e-MKK=*jI|T9(GhLHKvISBUD;_&9^f6EE5~Ft_xfB4>PJfLYy%w%amoqQDJ~889a%o^$Dan%hHV&Q|EzD7{^er|)OU zh{Ue5hP2}==+SU*ir0ICk)aRj9*L%Xhaq!rA1K)k-1N03j$1cP+i4{Fdqntj2QJ-5r%#b|F!>y;0UjAc|A9;n&IOn zcztGpmwY}|BZIs0+0q8e=fQs+ra{06aOd;MReGC{SYW`;Ft|c;Bqabc%syQaU`lyk zlmSb(4GOxs5GH|9uSJzmtU+;laZV}M^U|h7X)mAvt2ggg<#@)RRjZ#;o6qzZia@@J z1j5SbCY(D<@@YR3j^f4gU;!t#MdpKIlJ#!IRs{|r0s!BxckcP-ktmp z`QCcVf2=_4hNTwa!01wkYAF#f37UCUTj2~`0!Id3UKE3egy#0H*`=;^71GP%MwPH5 zRw#=w90cV_Z>gr6(JrAJm=*oA;b$ngoR!c5AXraUIEvIScyVK;4uFsPJ}Bx|g$N|_PWOEAS}R-j9@ zchVBB?0VQKzl_py38YU31p|zs%?d~5Odgsu)v1G@IM8}C0 z_sQj=lxOF=?zm@f(&LMJeAk9RjqHwez5e;()I3S*F>UgV#PDLT#~BwUU$c94B zKRul|G#$59{c>Avs-=rsZZujRi=R3jKl4oTOeAq8vTBdsFW~G|$=basS7p-GgJ0a$ zbGv8oW6P&SpA@Z5oLv>=)rlBnh)ev0X^rC!PMTh~tU9|tcUAt%;eH^~)V$Jb?YHnZ ziYh7G*;|g=-*wZ&PwX8O9w|ilQ%f0z+uIRlF)~_qUm;cQFA#YcYf2DyZD#Sz6a!F~ zu1r<~E2Wx<5Tc~9yc>x|X{c5?%uZuVs%m9gIt7jthzOzid2DWnNOQ`^n^ewC8{m>~ z35`?Y? zg01h}%08_RdxNe%iADo^&$FusKoMyi(elw6(6< zS|7-?62JUduZ{m(zPG?~gDXTdYX)igV>5WW@F8aqJ1}epi8be3B5cVlFOLW|i#b&7 zXzknrf7V!z@>%|Uu&bvRJIsHccOf;;TiP;gX%Oo+RYD-ZnV#hiLfGi%gG?0JqF31E zn=(zAyS4Yise@avOqreB!2f_a2*D|z#m-n{-(aw@j^`2CHnacFEpAdTkD#3ds-GLI5@+G*2ffa>TNuwdH)LPZpO{bi1LThIvqLU!n#r&s7VJJj8bg^S=&!&-)mMzMcdo z;=w_CG>QOBbFXn)YjdtN4FXynIhlC$S$(Cty0;!SC$OlpH*@2i>+6p1U!Mkf#84#9 zKBDIz=1*CkqG6zvIti-Ax$Fm0H*v0aPb5usDq)hv74 zg$cYk>t)dh_HoI%k@k$B@W@M#seV$3LW+wnhJ&;@9ExODQ|;rsg5t%{jHsHcA8T&k zKkJ=bI>I^=n1#|Pq?^k0RGABeB9Ar8ZRy_XEyznfto9iZsy%G-Y|mN{BhkgV^Lk?d z7lAJaYD>W_VDPvB0}@uTbxppX8ImeyWp7@oH48ZEHhX$lnvxZb$cys}v?otG7J(eS z$$<{WOOT&Y-7GPF@6s6SbGkJR-n7BKG0gM6QU(6N(&4WGzDP8(Hxn0@&?op1Y&LV) zO%#J0OdV8SD_}K~ZHFO2r3?Qx+67V7mq~5_g#JR=xJr3hVLmbRGItd|$0tCljHEej zTa<^6pli9Bcb(#a-&W?3{6S3d3_Kc8)YRUo6sWLPcVqOO{3CN>M7+EpJ+3^liT<#j z?9x^aqlE_;j7&U*)QNbNn3xKFJdmtzPgJ)js|P=;9$c9UK46|!FE=#tORWsx28s8> zhaxY5dv_=$*&bDec+ehi4mk zmcflI$h^^S%$YJb6Hj# zlK8@+h|Te!y7Q)#FhtZ%8%oLI0{OwInX>AUVva;FNwlmf=0N0%M2i$Q(Mey2@# zt-(ui7+h6_no_Evkp?rxnakZIXcV0EXVh3(W*(y-r6Mm0%On-FFawNXgL#fx*!#7x z@;{)8{4&-;LP55lzBB3A8F%b_dotO4IMICgX7llnV#&d8B?iBBb1*==>{T_(rC-<_ zua&%765rnYQTM8C=uUNAvbr---AM+O{zO;*t?K?|M~XaGCK6Q>D^;hLOG#a>xpwN- zwmQvxl5(DdVJ_vYPdV$pDzcW8!U81hjN5B}UA^n|f|RQ&>DrB7+_n4qBgwA8MAu-l z>pA@5UC*T&y~)O&L}O2~u`kis_xj0{yXls@JMM0}?XG$A{lyx*wee@&CN8y#~iKKfX?w&}w8(0iX zolXX$iD2|DriVX9`#oiorj)BO-rDmSEIWAD$QvW^hG*jE0;}$6#a*>l$<=u8)^a5^ zNqa-W-mq$K(ngCWYI1s{5T9d9r3D=;4RO=HLH2=_PE>oRWVoQp^>HB)A8W>q36 zePuS4mESGq+|9RKM?QRM)ioNQI-8ujkeIrFf@klK*lPw?Y1%hb!rN zAU_LQeEE3sSR4P7lHNMpe!7PrYby9@+abjN>z=-H+&+1{2H`(c_ZH&zvz`1{Yr)SN zj+Nly=N&htH--YngR-VG$6f#4TZs?=67KHzB4zSX?R4w&9Zrsri!lE*B(dbu5x6Lf~5$FxmUggKi|enT0Zv~ zn>3kTvB412u?)>>%9PLNOPBni4UMVWi<{Y#H2X#+tN`YG5m-bSYwma5?YDPRHDbOsp`F)f8LQ65#S^HH29!qP?Nh4ePO zQhNO4@CY3*QV;S-e~E0xW~`yxv=y9OE+q$5y@~+4NBgK;p|-#&MaF3drGVAFqWX#J zE0j`Cn;P0WoN+WFy;vXGWcAvjF2gxk4rn1OcP|&Fx(|NvJN0lqlPF9!LN!+?pbk9cLa^G%OJ5o;PEP%@bxZg1?bkT#X){B0h~51n%cQV z6Eg*1M?Np{Su(0?(Hs?fCy>=NZkRLD<#Wm%pmL)o-4w83UI206EFPWja*|m$pJDo} zVFsJB=mg<2>xKpxO5xSDh!cT}rUg@o4uSOOyTe2vjES!+T-z+8W}=&UhL^50VG4dJgsqLo(!rY z#G%d;-q?#e?@{o5ncY;hp@l)eXn|lORZtD*fefVaaXPpcOnFmboB!Gf1FIX`t;mBc zBNb@RaC*?Y2dA7oWq~aGH+*;AZF#(#f9Q9^BR}6-B4S;@*eoKTS<9VyB43%Dy%IiO z8Lb_w*3PMXWwP2apJ`0nsGo&8jAyWzAZE4}M`l&00Vupc&Dr1nA6(u7Xy$CoIjzB! z!+(+3FR6N>#Ll`(e~wAO!71&~QbrCM-~$RjtrP@9v$OtycAr*k0l(UgVw=`tY#$kJ z^2Q2L#16pXHL^1EFGmo@OBgl$cZ>us1r^oT4t($I@_-RZb#;Gm^!=mBu4nLzcRlm^ z3y^PbR_=)hrjr3F5s)C#PQz;wA}!sPhrWIVhcoGDN;sNsIy$dEb@RY*s%mGl>Cr^f zqqnLag%Y%R_8mvXs-yn%N)MK*?>k=K7q@r3>qEfEn79p{A})|<*HYESFVmD+Ngs~j zY!@_m$$}KGRrLvYi^?%o^xskJeFQQs8|m4RUc{p^{?9!xY%QY~V^h_}xjoo8Pq=zk zT#qi>?iFw~jcZnmtN2&d4Fr&iq@yw6XiPfx-E!>v%F30v?mTkjgYbU|->3NUz~|Ks zcdcBJ^Dc)vzbgfAgF05^e9iu<{rlB#53f|UFWXmb9Vy55<V;`nNdKyi<2WpdJlJqX(Td(xH ziDA=8GPcs2tm8O9M&XalciH`hRefI1ytRJY*AAgJTk~(4i9y0Wlv_F!tKXU!0k1ebA5zUgAvLAs54RfN|lYsZ0UiSAz)}rF|^!fE| z7t%B6+}1QmhtSf73lp6XK=4+K-O~L)TkIl!!HBOKk&QSkkeXj=*S%2*F*H6lqJ1p{ zGo5=v5=sK8N4dUC5?jGHbF<4b{KMGAt$`8HtQ4s3bqSo7vNfbfLL;DySPK2u2+KBK zdE9yFsZYZ{~%-;Vv&xBob3zsD~%Wvo}(ew!JN z!hkVtZm)y!5)wCcWz=O}MlLWVa?;1tj({LbZulc$D*Xes_%j4sCNVXFATHiVF zZuh@G!lLP(p*?odtUm%epciAvihOR^3oW7Fvbbu--{=g>8WaDa2uLFNa!qE}8 zscP{53x@n0^~ez9P1Zi9vGwuk3Fzb3&j1P4jIj(Ml4)v#*qmvS(`GE+c7krd)lG9u z^2nU89C3Y38>3THb zdNk>JBH?;s#dU1OaXfB2o(2obv+G|>)Dx*kZoESiKx&9GYTlY;w{$cRqltCIYggU% znOwnSFt#1VBwli|P|vh5woP88s?mUfh!j_^Kp$ix2NHCk*zd=d)?%3A_*B*li-E9~ z+cK%f+n$B7gQ0{?xKI8VWI z6fi^Dixm4Y1wTQc*l;cZCehdxCYnnmJ4z%ONvjm3D7Z}lk&g5m3jPU!QbE9hRcOW^ zh~fOu^dffW(8aQhZ;Ij5PGKbJpBYmhAq|ok-;@aF_^ zi}$_-pZr>*?|$8Hpbrn-WuG7Va({)j_-;E_P^z5PL5KV<1WR%e3CD)?)uU>$y*2c`(YFIcj@C|mG>L4|8$o&qwXIfGnHzjx^IIG zgBddJwy^f0RcY-b2CE;vGk;pEQLfb}Cm3mhk=ACUwHaxhMp~zl*2f!(eY}xcV|_g(h2lDoZ#m*aa_YAn+i?sGv_({@l;`G1qWUR18*&-m zDmB?f&WmASqudUY1?ae#iqQ@kI~At1Y*1GHK$)?3D`6XkQ-tioMy}kh?cYkoWz@~! zYgEC?I<6F=#h?0xS*~vx%$4B<1OoO< z^!Gpa2@n+NN4D)WcgSG`y2XDADiNN^grNviSu(J zcZKtthD@RfZ}X7ZXAvzm6lNK+`fQ?&`K?2CpIx;3vcxR*Z5wjHvP#3Em@SnMkiOMInbsjo~d^X(FM z`O3v|UxirVs}w7JRbmy(mpxSNs}XCM-#NtlYQs0K?&ZTn*B0i&*TVB+low~hye}LS zXXmH+!QkxNq7dd!gvIcrI2)PggHb-n4;dZ*TP4m(b=dNoEH(`7!9NSsKo1%b{{`GJQz5CZXnJl5B41z%Y$KfbyQkSV^@f z1_qCwKRvpsZ+e@wBwIJTd8<;=Ti&21d}8CX2d-n`j4mx;ek0ezf-oBjM=>kz7lc_c z90*e^t%=(v$$307KQ%jjB8UZGk_wImqhVz&DN6~frC@B7R*~;|aPmwf6b_ulswhkg zqbZwI_Ah%`m5)a&W{wMTLDNgzWll6KgU>>~Iggli*}-D0^zFC7^>izhh}oANetRjWeLL};W%$lvIUOu#Hsusu zN@-3OmXlIiE=%oVG1#C;oyS7*Scsd2xc#0|PV}(wd=_57!t+^3Aqy#DAq6a?n1z(E zkU|zx%0kLmND&L!#X`ziNbzOIQds$mAHBq*N_rdp~N+^xTci2N+qtD#kHiwRVi^^7PmVku3CxP!{S;~;%bz* zy)3RRC5~6(+F4vjN?fhK6SGt2-zV0OnZ$-M?vP1r1Yqr!8kOa(08|B@YT#5^^}+uN zFpX8u&#GV;;1|F)GPMmfo{|Bv(>Xdb8|9TRKE$APw9nbura|&48It$$Gqck(d}Kbb z5Edq}G5M>DA}<1bh6Nd>&qqXlc79R_2cy6M&V6nC@knHjkbzc&A$L&ZJ2n*&6=&z> zjDUesMk&7Uq^vceK?hM9{Fw z;9@k)1MAE!0s2!pX!le^08-(vF7cC*YYRbv+8Pc{&KUAHN1f4`$l_cm5DH(PoeT%A zh66z$G*r@pOxJ>_Da2F#VK0B33RV-UsW2XMN&=@CVa*q=&CX-WX9@$lYmkP7*R!5Hd#)%Yy$$O>p`Fug175<^zAgaUq0Oa?hA%0I|-Qo&_la*VJyt zdb0ci(2bMqGM<&JGr?%oYZj;&fe0NyPdkl9P?T~5fyp^Uhh`_mKp;x_>RytbZJ%TS zxP5XlDn_maXuu-#g!bD3xT9EgbK!tK8STo73e3p#KtSmw21lU>0vI|X0ooRpBv&91 zoX7IPAO-@`z8$UnHe;S+Xk@ek&aYnL9&?4Z{)gne8Qq}Q?Y->l5u&+C>aqGL1L=D7 zzYszLIibdI=PYB~bD;Ca3%Y3l@YI098{_7cFOHkA%i$b`e_ll*+ytW-Oq8iUgyyRLlu)f~meZ}KVPBqm_;(oQeFT{U0xF)=t2S&CzWrSZ*fKlV*|~FghDyN zqzn_Z1)(P>q6GY=67&@V@-Mg~6jQRQfC2IZCTD{4(^!6yIl!X%$*@$X6|d(G&j+v4 z8dV5Bf#yLf*;F0d$_P(yryTAHU~7n|Fg*E%03%FGQ$lobNJRr+1{NYOgoOZd3!@IE+L)keF+x+H_84j-5W=1_Sqw~S-+q971Bj=6#vX*y%3#lgF*Ie)yw>Gn zf0(wLN`EOhZg662^$!tBC63dn}zwF(Exf0!b3Kmr96*e>`SbSM?Q5>xYp%VIY?eJvf+Ec*XwUvFB0BrO<$lM#=IU5;bsuxnW-4zt;8j`u8()6?%d5?2?w1a)xZckxUE6=; z?u{Q_`oX39`!B4Vc;M<<-`_{g>sqg_qf8ZRO9X>g9#- zHSp}L8;nt31er@1Q2^;mRJ^b_PyHMg22ql55{|J;d7vT}0(y_SciJOkuCGv`H0&>P zpAy5|RxpfR2tz(K4}pXADhQSB@yj1l6KG>H{8DBsd&t3g2WSQ_y-`TO9vW3Pyu%o1 zqTEN62;*xCDS$tK1uC{kF@hB7Pqz}VV4BdN(wIpnQ&sqHs*vMKBWb!elfb<5Ee;$EWjO8gcm{fXkCc=6Ev;!`VwKXdQ8+4S{> zguD5kyE$1}^;+&43#m`Io9?-r5c0;2*Kd6FOEK^9cz zKH%)xWoxd&SI@q3HtCs7dWt@D!(^FNP--{ zPYCFr1B%I5G&7V`;n*}70Z@M$O+KPQKa`|AM2oBN42(R{*W8Fkd#kB9ZbXBAdf3OR z2lV5P>e1MJy`B*bHtJzIY&-x)CiE18z+5IcsY2iyF0)u6B~*jp+l-nuLvM)fN_*70 zwUPR(9n6T$ST<%O1MFAJt)bZ}N~;u~Iv>JD=LoA+<8eTt>^Ic-CFsT>F1%^b!q1vO!r+R5&Peq~3ys%djw6B;LYz=xW#qDOXbPBjJo zs*)S`%xj$BI6tq=CXLzzv0>~%3B-e?J^t)o5Fhj`*v72tItewhUUDPE=cPU41>-#_ ztEVR|0X{dS$+QdhtNGY202Bc<%(x5Ca;;Bl9_*s+@@p#;`48y%RG-4(IG0ZzC#e_@ zF*2`6eIYYWQ2<00qDrHwG79#Wk1z^FDMJ(eYgnBr0cN8l%>x0(NG-_snRJ>Yk>0`O zW7`xE{le_R0%Zn~wje|nA_CDH;9r7s(-Ej1XRdjdPi#{Lh(PEUK`_di1tI+WVi>Gl zh(vv$H09{!{o9ukQo8~*gh~oZh3Rs0P)<@|x=a#*+YXkouT(sb0n^nmUMN$upp94* zp?Iy51rrmPS;-a(UtOFA38rWa9FQvnXsjf6T34m)87N!>*Pw9~Dk-ZQbY2uXeNhNb zegRaQkcCi#q~kgIB;+eq1fuA|CJ`)SC(+wQ0R$t;F$h(a9GeD$G{ur-KJtR()H^46 z6te=ofrJGF+Gql;dVxpnvVfu`m$47R z^AzMp9<>r0GpRsZ*dSaX;0}nPD2)ziT0F73SNmV-PvkVmbD9%5o$;K`)t-~9IVY2#hFyHz#jm>R)^>L(VjktKR`y)% z>eQ-hTCFVM?uom567Jr6?q01fe{A%^s_Wvqp}ci>VZyyT?%w^UGpp|XYi+%+Syw%K zKnW*29dS=btaI=^(|euog?>2qgSo`;e0+F5F&vHIPl&F1#N?;SO?}QQcy;cTxkO%d zJg<7if^jOVB$7Q*+8ZzJU2%cve`DhHi9~U0ytsA6xt>#cFQ?{K-@W=Cty3wZTv7A2 z);C)d<-PIp-ql0H@$%snHwqx5Y&JXuN%N zwd#D#QT`aIE7*uP$6d{DTkiI)x-O-ED@8WNbDI8i?`lr3IxEQ>cOqv~C>!JExVt&# zX#RDSD1(=e+D{dl-pxK%gx6nGAML>FKe&(P*UbZ5nhCI+aeIfCE2FtB0-UD;O)7PpCQMiPB@GU zX}8Uq9&+T~ZrNb(cN^t*{QM=wv#=zmkoglMN1=4h^=KbDxRnG)r>hy&VS_}PTAlVB z_zuJsrVwZYP(m50rzubxj4q+UWDn>Ptt4I9IHIWr)pU@Yn8(45WU5!EI!Hb_?itCz z3AJjpGbkGSc!=5>T2op|W03ZQ{|jp=pNWG?DJyGyVT1&vY0m*rETQLE`8Jh$njWKX zLoYM%2qdu|vs?+a8s=y$3;oVe2SPQvNUy$I={-0njfVv9pd~;qjVjavIlpzhcg&nR zk~ErQTI)kVGbybnHaP9^r*J_jYF#bm7WLax>j&+cag9)$)PIzeVWz`u{V{EoR)V1y z*!}2*ru*5!$k;I-T&A(1c6hUvFX(ilVnC|CVP=olat}) z0Y+|}BS|}n*qOx&Q^4{nF#>{7vtT*g*tTzqH`c&Hyo+HBJ}b#5TQ8{X(`--r3?vlG z)7aR`h_^s!c4~@+eE$xFgQ`MW3Gwn|<;FLLc!lJWid9lTB_o)~e)(X=+DUpA>SOh& zHa8$MLG&0bW`q#h?#-3SEt+jvozzP{M)FLMZ<-teh{9!Zh@TOz!I7+_TV({B*P+uf zX)stSgg##*Vw9FkSs-2(qTvusH9@ekfrt{IfZDo=Xc z*p8`VT?gX34m`*?pdtHv?tRI&P9hW953PDy*F2?f z_5Gdo$)kZ2x?HxI2=4E?-*{~F)?4fk8_4Kvqx+MH|v>TbJJ z_@JRX*{~O1iH5;=!yxkv#T$k`$jz#)`@q9Jb<%Xpy2>9)@(l_8={Wy%?C6EqrBLko zFaC)C(!*k|q4#k)$9GZLuD4vVy)&`T&#&^4wIe6fvyH_rT#ZG3Z^r<#*_%9c z@<){i9`5DJo;E$SaYdaQ`x)6s)YCop{$$%eMmxP<*tE9q&}%1Fi`p?BiK6a!QFpB8 zbMJjFcHv^|+36n@%~0lMIG$czUZ-%o+^0nK!VOHP@I^S=qw!+YDSVmIGjjq1#8+GK znPBC<-(h8pF?$aD$?vf8$HRU9&tm1Uj+GVrZx!qH8CYtbnQfBLrT;KwF0ypz2?#pf zpmP(7CR-?eI~0w&v^o?NJ%#njkmT`H5z|gl^d7?fJH*jxyuWPBwR}8d^;_d;V0K8X zNc<8Ct46W{Q5wM3Bj))znH-X?Y>TYZx*+@k<@`f98Bnx{;E#!- z(~2iR{eM4-KJ!~c(Lzjoag|^CKaHX>kL)3~2Bf{~5i}&Ani#1s$G~M@Dl(V9G&U%e zDPZKgdAZV0(mjYuVeX+w_+X|i%+l(P4#VI`7q2m@lP%_xsL5ddAr&cU!;>1B( zT;NL3lXbpsIzWdcZ>CuAfcJ)7hIhESSgzu9l`Tm7~+uH6D73v~cU&|u7mZ1ni; zs)B`DLC-WwgIEHp51c*AqBCs^IMdc9t57nOkcsj4aD!hbg~)`aLKr`nxt>!cu$Z=G zQh_em5uSmWU&tD>D@vp+*6nG^=O>Cr2y?`*iHaYAo2>ZA-x=~{uYILR4_|G^lo2y1?YJWl? zoXH%=?_p{mD}DK$3yw2pzk9(kX7zCAvlq>6+@g69N~7oB^k*BF@R);g$#Z-PJs-;+ zHq#tX9ShEd>@lZS50rfnLA~rZVF0-?=M~Oaj^APY8pE2SROA1|Z)2%R(QZx4k=hQk zQf_*x%;g@_%Uv|rpl=J=vz#|S_>Zx?Ps|3V=OZwwA;UVunGe~bT{(_OM?FZ+Ptt)+ zM@8D4&OxAO{S-rVi!&t64u0C{KV)ZWj$}fRzdZB+U;q8?s#t%!XQV!UmRsdJJ?MWUl0t z*eM)2tCR<^4H8qXsaXgNiVPXFqK2gcL$Fc~PKp?g_CR;#KChkh8KaXwQnpqAtdZzo zVF8;Z&}*y2>@H3!-C~v{Px^$S1<**&hlHER=gpBtFRxNTSID7qC70X>*0iW( zWA#e8N)__Dkg`#*;!WuT8w%5P$$=$0IfK<7C6LVIGPEpIJyQ|HHu5eC=z}ly?ND5k zAumI0e}vr$VNE5MyDL_{JL#&sbt38FZ`EyNSqq#i_77Z~r}QSj1m*mJM2=f|G1^~%~r<-vI6!Id+~vZ_Q`TfD4o zWoX@9dC%SOcJ{rNgGlppcjY%OCOwr2Pkr1|pDe1mRs4;bw`!6_<*~{R`YcHnRoPwU(7mp=1W|IOf?v2PxXJv|cd8u^;(rs=Kz6<@6MnfKkL$>!eIZp1we z$_t^)XL$BFmrF-L^wq$#6taMM@(+XQNPsvK|MmE>dedp*u{qmjb51Nm> zW>+#Ez8m~z&G#xEbe_>O9+ESzyNg$HfBnm1uFP+WlC9BS+S~*Ctbe)74)-(C-k1#9 z8_6l3-(`dm)87!k!9-vVQbrs#+MEwLIPXku(Cghr_KIJ; zBERROEOxVms1gM*0%<=ZkU$2Z*z||^FnVCafirXru>v_V?F{m!KYp`H65t#GBewi@ ze-BunB8wNqFfiZ&#v3Y zp4s~`YRY|LO?hLs)Uq?TH5sp3%myt{+PFdUV)xsOBk2c0>CZBTw7XPd6*hx*=q}2ZE6)K)^Fh_X zMa8Euqy^p)zJj`iTjcyPIbS8`Pv8s-{}AqSv7DOIMP@s8nERhoT0NbsVI{xSb`V&UdKTRG=gLJ1W$z<2CslSr$SS6@{#!V0w_FJ4AU~ z+piGRiq66Tn_AnLs6834J$b)&DCVeMKXy7cGQE0i`es9-yenSbb!TX`{7BMOmW~|f zO->jCCA(bp>9tV|a4-gSPj>$`d1i^h6Sy>~cv@oH>xDt2Ky zHhq1A<6bnKG~?wnkD5QEuk-Zvf@zFi`Qzpf$!homCzo5P6&UlLg2aFP(q9e7p1l$~ z6^I4qV%M(6URaLJ{T?#iFg-(g{pJ~ZJ!77wSIg(kA6U$}c^eKcJNH%BE3Sm2Chn+N za}_6Cd*ZG=>z1n(rCYn#jn@=NoZ1>ZYGxGM_gX%*`Pt)7(*n!~(EwFuVxD|=*|J;M7QyaT- zbMrp3d9t0KrEY>DGT{!bqJIHrdn9!iGcEi}O20`r@jJ|eVKF}b%$O%n!Ex^28s@<* z82)@HgnvU}giM5gOU`%5 z`7SvGnxB9hgnv(o2q_5PBj;Um+R5o4=lgJ?o8Wi^q0+~KG!gX2eH zmnLJO>Da}Y*vt!nuuCQ%0ooz+d4l6hG5~+kG){r}7yf^N;}CAiQrrFTJV|OxW@H)U zwI=-tc}e(1NUdc%$^dckv$)0twyzf9HIyy0B}D3P5Aolpg+Z|WS0uCDYs-5`&bLpz zTlnte_sYII%sw{@YRLkX@gcvI`H+k!%t`Q`Nle?Wou*^w4Kj#fRk0oH%1$bri^hAP zX5~TYcGa)H!OtiqGHd?K`7*#tzeURjE@%NlXYfzlh{1wPts{))F>WeDTt?w9+3OS8 zoA&5@T05-Mkj9gi0H3-IJmcdEm70kdPJ47dZoz)038fLFNqf>t0~TQY$W)p|zGJ+3E+!2j0x= z_W(2IkJ&SxM#)$E1{|6>MS+^4-DO;@ewcOAzqfF?{AdU_=HMbWUcO5PI%RbmO3mOK z6iJ%~hIRfzH}aRctp9|jgCnTjI)`!qKck(M7?z6DK|YhtO1shv)VZBq={dA0hRLK# zBMY_ZUXof>{;KyW?Ubbi+3hw57(Ht1RP~$WQ5&U($P@svp5A=PzPLcw=rQAdsUX03 zI7M;CSRkPn>4XF1?1LjYR6bGW)w(ExpoPq}(bkjsEgb=OQWy@W!Maaozy34)`k1S& zVzudGo)Rn^qbwe^YL$5u{sO@YZa2s&i-Vz1K)H#?uzC*djBCr7nSt{c0n;I=v~$@t z2}fhx(YWq8c(*$NyX-^58^q~+V6kR9A%Y|O>}veQbE?486Z~a$})%*jgv9Gz; zU3CeUH}3MT7xgB~+V2!4I#0(tPp5dh?u;gSM&muB$ufg?uWELlEZcwAnmBMNe&ABF z?9knbWLaOVA0_OLwH{SnuxE*;gYl+=cU$i_olKUs-Ek*6j>kKW$4*?j-|;M%w!V@L zi#xyIBTIg6-os|jRr3V)Z3K(pNoe11Zp}gx478F`QgR2rqd!^nebl3K#Kec`V4&jK6d{lPi*il4t2zjQes zxEi~3^}+c`mP#H9sz-ft;bBz6F}6a-Oj8J{qlKdHh+1aGM@Z>f=YDd}H72Mhx{f7_F)n{bndCUcV2@%an9#x&ST0t+aIPoE_bl zN~+9-MFBUkgrgZ%cXYH(SW*;=s>KE&x_2K|`zAmVKaXPmtA&ihD2W7n>=Xxf7Or=j!+9^@@K98;TnoM^GjBxF%ca_c z2y|L13R$P{pQ`zp!II=w>rl<7^Yw-(K`I}p>9m9-%8(B13^Qy-j%*tfPYkqB1WDYz zoT_yQuNIk{N^u%)g_0g9wKj6>MR^;X9Re{A=Pop!6G|2oKPaeM$x4>jCd%96n`A4EGmPP8MFNQ?-40xXYsI8TD9 zbnlihj6?!$#O>Lk4uW?@NXp=y*;fCMgY%}yUOH??0`_3{$pDS0*O$mDMGjAKu~ALS z$K;DRv@@fm*jD$ZLQW}0e{AQPI{^jC4b$!xo7qDvO&tv7|WLvM=f(G`4b zHTwE-7&^pcIQ))Tro-=;Gpkyl{!Y!sX0S8;DwW{;k_Jm@qU3{~b4!}>CfISE>|_*Y z+SCb)s8Kq&vt5k|OQpNRAwok@lGHM4kETsNQV1$6;SR>!!MhEKzVq?E^Q#ww@xEZp z5!46qw?3bJ)HuUd!krmrTg#hlHe2*|sAInKt#ne^O0Z#ey+jy+P#V626$rOaR~!j{Sd+GjjqB5q^z^=yl?QJ)hw$q?3B7v%o+RsUr?8SCS=ZFOdqe@>_6P9`UVG8@A+YNp}+pAUHK2l#CA&B^ay{AHcC4b52SA< z)*c@XMG9;pgb{P>)`#JfPl#Ya-tDJjm==K*N%cBE<bn9LErPJ0-5+!Gv)*`J0z*&?7LcYxUc!uCV6c|yLH1Gv6H4`v zkBvU*MvgGDk3a^4!2+9snsgAGwx)&qlpa@OaNZ`mJD>WCjyVH#UD6_MqYuz;A3P7! zb0x+d2u#fiQ4v3z5uT4A8iNsvOu~~rB}A6P^Ei4X6)(;UkfRB>4ZxtnX1Br%BnA+U zlLMZe6Hbxy963wmM9C4!p#xa5LWk_#ga|o5A%}^}9CXuDSP++l|4w1VYzrTe^LOMB zIAIi(@I(3}lq9f$VYVDJFhUiQNX0ZW_+^MmLo3mfe8Ue+Oor#?5TyR*g!~Ec*GGjk z!eQ6@KK-<8npZ=i(SEnXzBK8Vf!9COt!Gf5YYdgxmiUuH=2L<+0Uf z!jc)UCN$*)`@F^=e*Lc8~c+{hEI?pas~wXRKy4s0CTsI~E$P z)oM}ZMc-KXI}TRzk3~l7v^rJ}jMa}eXbtV$W%qvWa&(9{)}u8xa{D>4rkN9imm7wh zW^I$mcXIL2^7zzzS``*@sill8EF`jtIaw97d0EKhlGlV;IiW2nvXGl)b$SsC#&hcWFnKfnn+#)ME$0wGm4U&9)G0tKHNVVk3rGON3gqT2@oXYU)`{Slq|z8dzPF>N30* zxm?F;8(D1=)!HkqH)m{SZ7rMH8d$GZ*0P6Mw5YgOYnsU-p-kty(Sz(bPIj8R z;yChSj!U6-ANMvtkF%ZRzYmT?&JQ}UKIiWp0Mfm*YWhBM}jAaH*wgN*W zuSo{eDFcf)^q?dqve_JvAxo0pz8z@;l>jZ6T-}T8zi)CcIKR918D;m>b$YBEVs9I0 z=8_h{(~F6q;KnVy5Q1i?mZeGOH1{qxe5rnjHipy=0XEHL%{LB6!<#)Cd!zAK645(L z=rZ1@pT~N;62qI5~yCcP{(SyN^6XQ#&Fe%-AuEy{}SlcY3y z08h12S_R5U06{wJe zsW(8>WRr5HvLWu$U85?H;DoSLD=Z&>A9&iQY#&fzPHLwthKWvJPRls z;Wxg}ZEwNXa<^&C*Hh|>-*T<__LlZ{)3c>?ppTxhQqRF#uAhZV0;=)oK0vi?rA|D5 z>k||jZxcJv$uV{iP}q$f21y+_lnB7|8c?os1L&R2sR}7PG&JC$M3I>^Z)^}ckW-qG zbx{iy7%h6-WBfCY@=yQVcp59S=?H*C8(3Lz<2+awY|3A>SQrgfnz4I`yhW3sB)fKp zUN+rr>Sa^v4!xLarcgW}sIGbzAa9E|ia*>I%rvK19VGD#8*=i@ zu(BWR%06TuolYv#UKr{0^f1!V{fm%=jD_`mRXD_MrQYfdWH-5W54Z2YCnxTm`0T`^ zk=4DY49?i!|DgTT_O(~eK6b6{zhH35p6*Wu?+t!7u)60c@yQdR^Zr{8CO(~58ybDw zvD$T^*d^xids|!;CJhdXbrfR*h1ftbHkyx(e%@7lb+qv6C~?)^UAT&HvlCZ|4&td5 zr@>PmMTRAn9bfqt%yfvhY%`P7odGjZ{;lzb%Z3$4ncBbuFv=vf} ziH#=RG9}Jq8KyNcunD3cebqOSF&Yat{-#EF66?5swAlH2q4V`(=dnWPvHbB1kCoL< zvDi6X=$y`9x|&a=S3BQZja|Dn@>dhjoLq2!nNTnkOSGsTk&vrJp#uT{egW|F9cP-4 zJC>WqbJ~15J11y4Ap;{=&I+)T<#B~9uN9s#j!H*pvP>9gQZ&f`ML)fx3gc{nc@^DN zI_BSVA98L?~>fv=-1!)ha9vtq@XB@J78hdO<8|@hc z72Mc^0-10Z@!XoAulVJGDP;*(q~V^J*su&n_%*XjnamBLY#YRRCX^I9>q^XqHd_$Y z2xb$+Yt)}4J|mb&v>5Ivggf%Sj`ChC)*})i=GYLB?IbJ6ASG#`oCc6GjO^t&Jr1Z! zoVF7(+wTprM|mB9V(HuCv{z)JN~lC_*?1LzR>NQ#05%1Ty%n1Zopx2BZnO6>K&ZqM zCjRdH@b(Y$zF2wH|L@p4MJuSF$QYq5c5jM3%Ig3WOWz)Q=S=JYV?qwrKYUd~;91&u z*7T`Ott?!Hvzij%{LCiO8FID_VF=_B9=9S_uK^NMvL+`rn5?lX0-&P(BnH@o<_~Uv zkoUEfw`ZZ*1wclmJt*u(q?KNiq1ZyROW7njZ&Eg~ARA)Ap*vMgQNEpQI75rH-IzLe z<1-G~%H%pdzQBRtVXT-~$x62Qcoutr6Jza!gheHs2`|Evw_+cQjwwgkxJtpWBhzf! zQOyVw9pB>@ozqUs6g$W*x-f%nU|P%JI13{6XEa`QQ_Y5f=?55l)H!UQ^|1}PD&xW` z0F28XqnqAmf)?aO+d#Eypv#?8+!SA)$MAtg*9Bg557C(;!Nv1HV|yV^eajug^DZi^FB@FSUd!g*&rY3<`*bg?RMPvp*Nh$MUV@j>DMR9ry=oYyUYN1x&Ycl; z7r85Ow_$Bgpk4Qx>(pzFdCAqIQS@l}y*xitl07|wAQ<*(I7z6ea1W~~+buARls~}S z@aF1U?{x;Pa8y`lGkJrB`h-#%Z8yoH8Rr0uIcy&rk< z1E-(+-J#lNK`z?zWjMMEf0JdgJAe0k0b(Z6X~#%h2DiJ&YaVA@u8>lGxtjV@K&j*| zn4V#j9Kz^|^I_^A0_#-#LSr@CS=^OmtX zQ?9FOS6FaI-bc{LU@}?>OBXAUW7ScF#4?E6xf$E5Lv(}>a<4k9@ep`&vPj4AX+$|v zFo9W|V^PUnPp6;(2(v@r>eBbEf=wnYl zKC#+#F(0}3Ww>EGeVf{V+aQ&*JAD&Hi_T3{?h>7YbTn?$RWrmD!+}BSzzPk_Q1)_) ze;1%CDN;u9QBr8gR$S}!_+t1`*FS!b6(7T$LyfT;n<&wb4g&ANjlXBQ4Z6hEZfG&n zv`ttC%$n3cGZv_-|7@5R6Q*)Lvp>lON@mc{p7T(2ZV{i;hdYrDdT} zZDyQXFlViz->z^+ehx;PVeE=Phmo*nG7q<;8H%~Fg!U?9iO$!}Rim9kb6b+NHiS&< z-MwT-(Dv2mvc>|o+|ZxUp>gl>zDe2OUAh!ux{zqea(I_p%e>2sJV7OJQk1yd@FL=v z%cj)jUIujvvB*P4EK*RRXXt_^h07M=e-jd_GQe&&ln!jv(2T@u3QQ2r2*FUihy^JX zGK%!Dl*(-iW;zXapCzz&k*P!%2w)mIur4{D$j+G@&sMB=RblyUP;8_J7>O}_v8J<7 z(|LcSSQGzEO}rFtD25N0OH)EcFfkwQDWXTZ(o4Y~qQb=JKsWpQqM> z!(TPD-c1!_hYPX8pC{I0uRrNKR*JTKJoeGplW5!hRI%q+q376I^f)Su(cVI|cf;%I zuV=$O4{&uY9|u1QmZEQ>nL)%0(fD((tFHc8U?+u1*dkPHygb6Akc-V+_LqwWsFVt? z!{_dQ#v!{84T#5f4d^yj5#0D!6ex56!bQnQEc#w1v z9W{$!@TyE$%@8g3VWA<{a{sV-*C!yjL$`vU?W;GK+9Q+9B7C|@R0hVUmixBxM7dvL z7Q9x$7IELs{4kBh>n%%*q<$sywx%3l(xb)&wr_wBV+DvS1*Meh0>lFi%t5%cY?QrrQp#=L&a0mg;UedoeqBhu}*)88j8L{ z1>d1H-(blXG~VL{U;MYezU^AjCRzbg?WP6)x1oj)Y5mQ}m_~EBrx1{&d}YXZmX`t; zmdvFDH{QeRPPSWx6SkkkKcgt>xh0k1=0J}ooK5d#V2EITxo=DKB9Xmem`{+k8U@=B zzmT(utB|Gxai`zFh1wb-0C&BW3hDC~B^WaZ%RIPB=B7@ChN##ESGXjr$4MVJhaI%jwfif~H3^$BqPn;i*x6q9?h0t)L9grmBiyV~kO~79) zB{F79SdwPbimGMO_`D#8X0HTYlawUmuGvo!M+NSrstnQ$6lYaVAk%&DGjVOM-B2h< zVT2Y*BPo|DOHxLnS|R(M=`}^863H#S5f|I^fkv`IMqui?ifT1`iwC*$O+U>e_yDnze#a{Rjb7b+y z7d?njurS4o2z<5wB!_LipN#^G&Fq2Fdej_wimi)HH^sm3LC(D(VaEg?BM7$dc(d|T z>{|EICsA~h<0FHFSv$lMdW-qRRAMnb01$9)CWDACoOF`@x<|XQh+gaq6}^5QUs2(c z6yqzZfg~>6*jFp;DD{@jt@00&@*+iRRp##Ndp9@F%NY=fIv`)CPXI`&>25eBs-_3= zP0IpG5~h|Fl5BdABGVop)}1`h zf62Y`Z(QwDF7}jr?J3vv!sX;0{}tf)BTu;_-*C~VT-Wco{(leby*s)Z=)UQF?mF$@ O8}hAvf8eOVCix$dm10W( literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/feature_motion_manager.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/feature_motion_manager.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87d6ac8e1c380fe1f2f2ea605e10a1f31e51a5a8 GIT binary patch literal 17166 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 KBZm=M;{O4Gn$r&e literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/feature_power_manager.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/feature_power_manager.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3359b74d183a4afe3fd5ee01dd05ad0086b239a GIT binary patch literal 18362 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/feature_repair_incorrect_state_manager.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/feature_repair_incorrect_state_manager.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e4180fc936d4b5b1ba93150fc5a39200fe57ac0 GIT binary patch literal 8432 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/feature_window_manager.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/feature_window_manager.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ba94b5393830a3d4f36da17c97451ba2aff1002 GIT binary patch literal 26199 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/open_window_algorithm.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/open_window_algorithm.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9a97a8af918b4054afc280b0087b8e3d1f90c72 GIT binary patch literal 7579 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/opening_degree_algorithm.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/opening_degree_algorithm.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3306d718bd6614cc7602374b4ab8df285dc8c97e GIT binary patch literal 2856 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/select.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/select.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac0d412bc94cc7a007a44e736e78384facbf79bc GIT binary patch literal 7637 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 Q!s&;;M{|EB$nhEcAGZ{x#sB~S literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/sensor.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/sensor.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..50cd18705783f39dc4bcd409110c6faab8c2b7a1 GIT binary patch literal 63235 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

8%Z|0I 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 Zm+@};y*+On(d$|RO@05sQGnIL{}1rO{d@ob literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/state_manager.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/state_manager.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53027058cca634a7f86f8318681d78997d496bf5 GIT binary patch literal 18969 zcmd5kTWniLcK7lj(R_%M<%ew960N6Y$&zh7Bg+p;B1PGvNK>>F$8s2oypkzPqH=i^ z$FA!d*#*`M6wU5Ow_BiHWQ%@mfD{PYpX>r{lProlXi@OSH@?b6(sj`V7F+Z~UT4=y zKZ>3+_eEV^Np_Om?SPt@JC8GG&di)S=gf@SDm+dK-1n!CExi34Mg1p!sLrND9#_y5 zb%%;m9Q7gRFa^A3y^Mxz9%J5EZXSj;13h&}}g?Dqi!+W?r;l146a5YyQ-pB0= z@8|Z1Yq*;50q#KfAa^ia%hiVKxVo^P^OJV$V~4`^Ts?`GjWvWDxyEo4*A#B%n!_zz zOZYH%nBJSy* zv6qTFyDf4dt}{+IP_3?;FHdqQj)|m}Q}Yb#pUbiSDTd9bxa=b1Ph4f#rCc5gbU2-f ztJ%Ciy_m}9{Y#{D-anh;uKKyFj6c1~vdl8)Pc3K2H_N=d%H%mF>CKG7t(TE|sQ70uvVl8Fch_)qB9v5G3=T(=iC`oq zZAc9TCX&&Kap*vC*-$(>nJ_7xxEP1xp%B0*YKNd13yuJc;?&`2bX`4Bq(~wt?ILOM;Dw99$wY8ON!=Zp98ChnMuRfe$yjtc z7?<`NlXX%^2R6wgSd``#V=5go)(yZ=PK_llDZ>h+Xxg;ML^Pon^OqDGnhZ`12lWyq zcqYao(C=70I2la9EOyCLnyjE!yR=jO48M{}M{*g44Ax#lnl|QDh724r$(V@4CG9el zBvTMtcgyK9mdP_5d{+#m@{GP@(+W2rfmQi<{BICR0T;YNaWrs3%Zu%tbq|#Te#qJ4 zmc0~bzfv|ub~2jDnKij$(6vg@CyMTx7mfXcW?B-8oW|lAE zOh&GR%uZ)Z&H%@;+1XW&$qzXF;1s5oBrI$InV~l2o-_EWiZ#5Qk>*TCdn3_$)=%C zd8|=?hsE|JcQTn;Ue4iu4ZYoG=&hc05(?+{f%o|x>Ir4BH#|b`&tHAh^-*0niL+gh zyJ0G04*?zbV?-MJ6@$>|U^>w^4~{aNAJ!$Xv<>T*3`|EW&9*}|wgWtQgk{`48)wD- z4ajfE8^D@A@a|CSWt4B1=&k3y^#w=0j`Y_u9BdEb*$-Z;g$0R}VoA$I1ZR>zk& z;jfsYO-RM2A6CEF_R*nU64#M~qQpHtMe7U{)fSAr6p^M!0b~hHy;?zeWl0A%P}Puo z20(12Ya{P%EI1l<)K$9LpeUKm>5QE;&I)7l%o8i0U_R?xOloPu0Q9nW9xu zdmeHRz~J41t{?LLRY0@;2u z%v^REbQSCc+u;mNwA!RSz<`n*Y+VeS0)EQCK9kDlmZciCQAXQ|^xdnvG*jNsq{h-5 zF2$k}RMbYRo$Z97V?*FczVy}9@;sB&p?j1R1>{cUvj|QeXG?Q43L%80?=5YW@KLF8 z!&2U)6j?Jj0OBuU%0Jmj`SyHVRsGOWAv&6PN7EY_!O^yMs7X8&;131fZN7hKOsp9z zII7ob>&4oWeC^41LicNj#p>a^p0$P+v0Zo6y>Uto z3*DiDBlKtob>JNRarN-W2NPf9k*mCRY{*XkEgd*x`@3q0z2~BbXxn>k5^to3thV=> z+?al12l|6H@ZYCt^sVS`*kNq|57*%1M)017^{_!G$c}=yJ&&1&QaX>Br0xBWV5heC z7y85NB=SK*PAKIuh}Soe$NaZz8y*7%jZPN_`=19{0T|-};LLc#j6*G?@*&_< zbPVt?x*4~cqL&QXBA-L)3N=R$W4Os0oXRyb8G<7n+i55OpK6);14#eD=nZs%fF`Wki3fecP zu37Dk{XB=Yx9unF*crK<-^O}5qV{J4MP7nFp%80u+U#JJew&nuq1V{yuRG5 z`t5NI+=+-QqJkyzDNsSpC<Li`%(r@<_uA04^>JEI>R`O8h{hguqOgnl1?rTO<0Qk#wq zsxb8sWP1zKM&{>fH8pltjp^Z({x`u>S>!`50x|iXT)Bkh(3=ZZ*Q`;w%%Evht|_d>(9{n@1zuxa~q%YIGIbRNa3Ur>}iG} zwg+N(a5_Wt>hzqrd8{e#Kk82{f{7u6hH^ig@^$!!iT+P)CbI1TNg$tWwjPlx;UJbv zrlFT?YLVE^SyZ@)c~tVC@j~l4F``RVT3T*yPO)aNIIE=!d77wJZ)s;XpH!06=^}X{ zEe95JupVc@j?75=v=Vcv)kQ9uTTX(hgy4fG5mJRbXEp#;l2o9w*|8N%dz37gi(t%Q z!3;&Qe$1#;&WN?nd}NJUTiGhn=?U@?1`GA>kGoW(90aEHYabFD0-I z2;ED1pp#zO1%}w=v>q(1~G!vnht0dfAY( zUO~BcimnFU)gZK-6kI3Q+&xC3iY#@)=jn}wF)2kw!Fo+{B( z&wJ`cPY?VGo}PDl#giA{SAgHe%i_fa{^G(D3S{k|<&hk$Q`DGc-0~TIja#;B<@v2~C=NvVfv7lei66KmT*(Ooxi#-@(Tiqg(R+gTo)CJ^3Ep!;bgr$zAoO^CG?#aeCLJOd?B|AkXPt1LdF0fUx47D5%P%O3lbnlc;6AB4XTfSL5SBT zdfEz}wztc~V-x(b3GrA4eg*htm&ELCK6@MBFVJHM9|M3Nu_Q5App)AYFZ94YN?K{+ zJxxOE8NqYr-4Su*5M+cBZ;}ERX91?4K_?n)(<7?iU z54{b4eM@Y4j&FJH-2*~PM7WSCEV4l2C3${g02Sj9ly-6B5RQHXWWc#{n6Er6w4V_w z&j=&ah07`Na-P4O7e?|W5Q2g)^!d6&J7p5R?Yy^LI5s4Bhu-DH(HVYpMjV~vN9Tlv zTf*qAPkej-%>7+=;b63IVOsD#uT7wPd&IHJ@GFd6{-`Y=r2atg-Fmojx&A04b`0|! z!(vB-?}!NQ30Oo>!d1(=YDL#!-gQ_wJ}d+;6^^|ixLznE88NxaCs%((Q7_R$FtLv1otd8=+~Y<0fA6C+yKGv&Sd)*;Fd=L_;WnV zY`6D3La)#hT*vT(M)Fx|u0>w_4K&vt0fW1GBxEAVQZYr?hw_?hQDPZRQ8$1w8(j?d zP=hm=0WWS*+e}z>tKiIxwM9ZT9jjDJ>fsFTmBba5G#9D6)slKh4##%s-)bxd!nM(9 z)Jbm9zwN1Wc8l6uQ>WU95k-CJKARYUXEb$-s28sjJZUqtFEJ5hF<&>l$NZ((fr$VT zGYJI}3N!|%n?XsNaHz7WiC}Bm_zi&Jvxqk4Ge(sx&K*WnK9Q>nXh8z#E0L1!+}bvLcZWtTv&R&ZBw zAl3aFvQWo)g@KkeC0D6~r_u%uS82>S$-voqv0AHCLGZsXowoP{9njgR-?r9B9x@u5 zPFAgGkR%lpBwa1TRmGwjC@ZQ?R8-~uDe4!wP|Bjsi^Uy-1#26{UII@llWzdCuo=o) z96O7#G6+*4EHUp{a>(F@aTQLk!WglysJ)h^~0~=>VGF8o{I6O zV&bV6_){+kFRlouR*EFTBfR^_TZ3ZTDZcHL;66>Hv{v5TDjYc@xX-S46p5tWytljH z=w5F%*SRA6Ktvpv<_D&Q%S*z*Qjz|qHgcM8J1w}+tT&rLZ6Hu(bp%Csqj=u(*0|Vy znr}b-ZoAMvQJB0WPA>41vP?gOqIm+PQ(e32;C&r$-4Z(kd}l!L4G{_XDDMMp^&Dtu z>-$Ym*iBG4%zF-to-^<($V$;PKQb+jr1+7Pz+4wbt`p39`I=s_W|*%TzB~RiZ;heh zAKemsgT^M^CQaHYPo=6$Bb9#CEw-KI+s=w@qkP+_;0_nb$qN0y$fL0E+EClMD*FA7 zkpmF?vuhCidyXL&1iw3(Si-r71`;9@pRxzwsucQ#z=d@Z;iq1ZQ%VJ6++K-bysinx zaT){})3ZJ^;TX14+U(}SjJ9zT5k|HUTZ=?uw7|nM1!2hvI1m>@Gm^o@0T&F~Q4V z2G^~8uh_Pk=eB8EH>#=N$jw6&Rm5zJfhJ~SY*VSHhMSIvc~4A4OO2-Tm}!D0jG5ZW zBIN?dEb;@uu2=3;u^;O%{*vPwbl^s0H4c=PDH@%@(p++K2{Ht!ZAKG1W&%^C&x(DU z@v%?azts5H)fUJtEt54db6V|-UJ|J{Wl?D|56V1I00U}CJ=AZCZaQm*^wMgqfg<94 zS?Z|eDbBLw3yc1ui;vHAsAV{e+VW7Vv?&vbpNL$`X+~2VN@V`00<)CEvx{d0qUivo zO_-f{$)$?#j5>_2gfj28%G(Dk8 z$BDVtdUggTJo_Sgz!;QMBorZ7Fd9&jJ;Thd&a-YzxPl3I#3MHf?@sU)3fv>k5S@l- zD3TSP1JRq1?|Nqa9@=w{d_3XAbJ%@w*YFhM#x&MOPR63a+j--$CI}pWy3zCnOF| z^MljE^BM8^1^)R3aq!xQgV#uYkKpThr&%1h$PZi;re?*dIeuzR9GL%bVE&=5D-H0z z0X&|I^C#nldG0fcxHJnBYug(!$`e69$jrcJv$;t(*t# z3yy+N$GR60uDmRazx+{07;;{v6QsaJ%i8|h@081@Tk_H1G0}6Z;5qhoLhK3iJz=pY z%J)P`gID>vg-V6}p?fyLeW^&#?B(6PZ@(h; zP4Ima_io9CuZYnMAI*r->+mZ?uYYvpJYaX5P9t`njAd=l{%^~NVvW3~ zQS|h~ui)t?XNQM*?_pFxPx0-i1n()L0-oS&PQ2YKc8&903&;3W+c)wNGwEhfhLSDhNt@m2lAr1sS|m$!I@#CN>FN8C%^PDeZ6K@zo<$7W`>d46E)O&WJVF621<)CAg4dm2vD)7glxcQ%eMg;S^qa52lIKb zOY4FbF|_zB(6-Q7Ws0_1ZiX(FzJ)3hDs-7wGx{nPZ+S=N9 zC%50c39tA9YTlt9y1eSqa})1sdh3ALI>@&U3a-J;_&TSu@oRka+Wo%82kxacFrq03 z&MmyB1?`!GeA}Sl*$T)kADtDWSw5P*-?#9>KC% z#>KuQ-?@cGKBc203Iw8N3Hyr?&rSxH%7$9 zUcRwcY&^|3p1$9B_7h*@8yT^AfNvfUn}_)3q5I7v>nJ_`#%kZ;B%=e^wIim;0VY~2 z@v-`HY6+ijJCk^U4c<6H93FPR4F9!2$?+zWb6Gaefda)W=ODX0nat$U$s`NrMoMz5 zusOKM$K4oPr8%Ga?XcjpH`xoFzyWt%A($!!|tm~|q}JgJ50{~KHb zgscRk)?@5n0X|YW{(BlwHl=Dsxk;^LJ0K0zti?sROPga^6vCu3?gqK{hpU~%Wlz?k zWCv{w7A7hp@|8db#&D04F9)K0BCCuZj1g^;+;GIrSH7$W<{IU2i_BCqH*o`EH8qgM z6E0R$*ND1B4y`1&f;5g0BmYZ<437 zPqE1;{s|W9LJy67l4DSwf2Y~Mflp+I`D@^t~Y{NF%`IthV GAoG6^MLe_s literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/switch.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/switch.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..655327a1d246cc5bcb11ef3dde77c39384999a31 GIT binary patch literal 10993 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 T@xY4I_F>W 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 N$)|tHQ2=Yj{|`>TqUZns literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/thermostat_prop.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/thermostat_prop.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5f4eb0421def543731388d669f46f1ca02c7891 GIT binary patch literal 17963 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/thermostat_valve.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/thermostat_valve.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dcf97cc4f52b011d115a8b8ee29bbae5f2370182 GIT binary patch literal 14220 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

3GEAzxSwz&fOn5ujh6PXmq@<6Sn^g z5<0E}*N`p`GqkL-5G{&sSx^N#(aoe1j4Ie*-j4br4X}o>>K90qWMP3fQ}Zxg%sYAR zIj4#7P?}vmoPkrXO~%p*m{Zo$lAK^BHAAK+3!NnBH#`^d&#ge8T9uTQOhR}S<_H0% zIHnKi5D{TYF@Xh`8&<%i2r*;VGRXvBGg2Rgyh8Z&^ zPhc@xgQ=WLB2zC&UdVvMu4T#8DH=ACn4-wZtAzeqNJkvqez+3T2bSr2YL(*oi)YWC ziJCYWL!lf;l%o^u?-z9a@Cv~W+To-f8Tfr_|8+!!li)%Pt#5tp&9A*R{pR$Yv3sHY zw|oUJpZ9jF-tJ9rFH85S-ahbEmhMu$U7OyX2i~S1R^AD1gwK66_0ibIz~zs<-}r*@ z@$ZgzkN4`BgfUncK#U3V+yA{=b0}j$Ph&ZDjD{>QGex_LF%~|gA=ojVs0-!A3s(Z~ zM!5~^iF%?>tEoT>I-#-<kx1&Dy*X;M0VpudLwwBjmqNXje_38G>=@QbI5Uu5gTbRrvFVU!N zB5jO_8OsJGWCaGPtk8+5zGH!0Y)LBVM^4e)*=CWXB$J%0HHuk0(qv@XAE-CQt*RDx z>{UsYHmi=+?%b{VvNGB(m|RWus9Y_Csn)F8tvNCMkEwSuT{X=Qqb|(2>#&+@RbUaa zjMEjBbGWuwzI=_{NZU#=?~F&E@^02 zrWtl{)4TT}@rJPK4d=Zhs(0iel$%h#YgFwTEd<-%PUJhDQ#+n3l$Q41N#y&Fsr|b`S2-+M^yMQKTL;Jr{jd_oPMFh)43?mbxuMLuxnN2b-0=|YGJ zkWuxy=+jC^wm5FwdB*vKJ~8vd3S-U>4(3N^)zR7f=%PBhxFN>#Vn!7+_eQh%(RFon zeRK3XfLwOI<}8GU9`5PP@7a&PjXnFJMz*x)n}^lr;e7MSyUiybws&py)c4nIRke^f zy2t1L;@3fLc*((d=g$v>&b%PR!6l1<1S!Tle+WgaRC%)(;^w)7Mr+1NOYAddk0$+N z^%Z8R^_B2u$Y`DoXI+Akv8_aboGA)&R(s;I)*PZvX0Wy4x=hTp;kr_i#<(olapd6= zb#kVt=viZ%YlCH~WM)~{yu+mE%P+FtM`mnRegtNb1h~nL6_R1NERa+%@3O>H@Q|YV zXn~q@0T0mkz(%b!XO(z`Mnyk1Ff(M>Een9O;pr%~;pix};pZq7$~r|Br8c~L%-X$5 znrA1sP~_|fjT^>pQ~&NBv+jBK67Fvs+l7T92VWR6VHdfDBA1?bFFMS-V}2JCrB+6h zvt~V)XY6D@c7|-cMUKJPaPHDul&zTeEOJMFfRjJVW=V3F&CM^J!(7FhbIf@JHz|P| z^E6yxGl3S@#$5paztBSLNDsD{3+v~6g>|QlJ2PUovA*-3Bj3Qt<(P;ymxm~%RC5>g zTX=aUU6c_o3u&CD9Y3K(U+?P63Htp%+uZ5T2&j zV+`i$B8xx|_D^{?rN=pFi9$|NBEqba$!1AkoST^v%n`AY$oeE3B9URRW(}VH0e2O1 zTZ1o5$)l_;EeV5tiLlwf^zAr%np!{qJN0BrWauk$U}<-6eXO^1#?W|$WLno@cRXfA ze)Ycy;XU#Sj#Tp`q^oPoS|c9vv#>A~7o&UC=pxG?0ajj*$!Y9{=1OO->rza$g7^EB z2n%{9oyckKWO^y1g%b24UEa_4s0&}Q6ryNMCQl(nTE+In@H{Kwr5S<{D}mnuB_WyV z*+wR0mcWX+6b@YmMsoT~-VL@EIRmjsbFId{jVPJcWkVuTOxBc_kqTEbYnSF>msI`} zYFDA35S|7MLClg>wOTR_=vFmAu{U|Ic?|6B6-qe;rkHD0B`eoD?F6tAfWN`mx!*wM zf~5?$;ouw9x2rcAU-_wbbm zeEl02Z(l3~bFhCs_PAgn^K#You&@Q{-&nc5a?4fH@Nl8ACEs>bZ996u@o2u`oZ4{i zUc-frndtq7=&gxg)HlC9pYNYg`zJQrCmsZw-s0cn3!z0APMW-G{bbm!X-sO<+fysd?*_PbSmzvSyaYwLQz2fpw4!RSU)_`L)7 z>i6B_BOlb?=MTa<*zVu(w!o?Z^Lytpwe#3!)v*M@%lnXT zzvI~Cy9&X&@6Y8MM%9MV`@zvdpa};0+Ihd+iYR&Ky4|1?uR)^lWaQi|bxQLk+ zg3ZjB_=v~$vSs(ERXrO6M>eaD&>;YDqjmge2RHf2Px!irJvcuB6B^zR7YCdM*(=Kp zDj$1$b{p?WLSd5MUm_9v3bNm93AJZlRZjA8$~tDBVa{79@&Q2F@V>>+JUoibx5im6 zU6d-L%(rNL%AW3nYBFNY&_yVv$^!Gz6W}=ouRGZEOgFjQdWS&(chd|axhZC##dfgZ zM*L?S=iVLx(wMIsv(6dh%7ewKP+uxu8-Um(r`=1(fKi_qXN=w<9iy*3teuGpkTx4? zBTy9uTRfo~^V|IzgWjskS60e~QE%h$XRY0Mr|4%kRp)xwDZ}THnDoxWGHBa$*em}C zBg%}P28{>9;ya>NhsVa$4TByUy17M!m#8HpvMza!(sYp-xz){=#*=~;qLU@x%hE&F zChW+i9W#j>O=ZwS?s&wm9|vwj?zkC*$akfF+ISqKFCPl4p>RI*(njc|&3!KwLjBtwS5w^+&INMFBG8H2 zJRes1@FqXRh+v<}_kGL{JgNfB7deu>ULS8g+35IrM}Xpej*}tx&m#j!!^;l5m)@Bx zbc->3z!*!GZ_}>*F-3%@@&!b&JH%t@xRfg0F`Y%@d108q{u&YUW?C|g#BWeLTO+aE zrv48#CtJ#IZ*re`13E7{^da1{j{U~%_YH0}GeTytq|d@HeAxTby+7Ie(aDX=uWg1? z^d8y&B_wSTcl_C2KTObN!aUtv2=rv|C9E$CCYCPG7~(jQ&az;KhtiSX4c>aUz&vbJ zT2nbGRTiSo9jfS4UOAVb7(wX`S|a&E&|JWboeByi8*{KxpDh*Q!i(g_Y6mm#7%rhB zYtFAcDk;9t7!~zjstlu^OqpfDqEXLSH>pWa*~2nt%Zl}pWIB#VQkc>h8;R74dTSz+MiR~Xa_a{eAjz()H(;Sd zkD0Zc{)Pj2+Z=yKljm6z_O@HvS7dZahzN5eLzfgox_(jULmi%mi=4RXzl$q;Z;nV7;}cp*;flDa;t5-MX+Y@Lc|y#4~M12S@Ayg6yL z#l@L>qxIQQ>|J2BM35ij{L7u&aj^BHT*O&E)DXig0@(k5Byi0%>@ie`_t4GHL>%xRjK0N-7EG^$e;jMKO_yi=s@l zUK`M#Ty9wY?Qpz;{+5y9(NX$J;emfWUVME^a%NphgJS5O(O!CqkYwcsd%j@GsOF$C&O$Ib>1dS``5ZADlQBwVS?5`GYz?CFk+Zwl0;#P@v|v+iV|~-)+qMWZ*iBv zG?q<9P=OeLz9W4> ztJ~QCcTCbfNJvj$7eotO#jlh~epbOQDf9f*0u~ywMczob(0@g?MnAYyEohIm3Ac@(wI(DwX4-?b^okjiI(c# z{jtw`X2v#-XHF{G;}w!iV84UGK<{jie5EG+*W3NtLo zG4Lh6Ia8-w3yir?M%bHlQw2lRx!_^(4Nv+}3GEiL|Hj`Ex zv^6t-`Skp&iKwi^r(%gQN!7xdEKP(H;W1f}Mw5zkR#w!o7K_UvZCq9+lGGyLat(~f zRB0j^os7#;N=crLMP*eQ$&GZJ20CVpGa{WEk446%cudvggeqxC^F5LAaAHh$$!Fz+ zrt-Iic_S$~DuEQ!rb4l(DuokKbVw?A1Lh#b!>T5|nMj^XNF)3;BfP5|j*PpEUICZi zuG^2|b-MHAlom@S!g1YsFdUDE&%|Y2>`YAc`31ddOx8kY`LaUslLpi_-ecPj!Zun z498SCianJR*qYo%NjQ;2QZr94DxI0)+hrbR;3n(ym*~z=C=s5JLm}N23K{2zvO5&| zQ%VH}Tv#!ZqUT_ED7nuuC508nqpOV7nN=A|iQ_4o92t@o0giksb z)5aw=HkJr$lZxyb;esheY*5fXGu*vn8RMqQppwnvy@gY2|z*WWM}1+|{N zB8&?`;kfWRR047;(Ni?b_BO%_GZmu+sHJ14Ex%oUIVcA0?F1^0KavvA?c-rp)yuM5 zqL(N#_A3FPP)gB2p}8t{YARxj6es;8K?EL_Ksii=HN7GfiU5(-Xe^=u9cbLmGs=4E z15^nofJlUcA-%ac^9J%8wW6E)GO{0L*b1v~92Ay#{=x7ff8IX-x$_Ure>k6hZTR2b zPLGVIV~MXul5@SOv@$u@JNea%XTN&xoY8cVu`XMW&FIfDBSNdO63r98-OD{F%&-&Q zgjK@=IF*ETV(f&JFcl^7NA>JDuM!;fzhwcdRqbxV*Tx|m2 za|c^$aJH&@LyA0+JPXBSii%&VMC*ux0D4{hrgeu(A0)&teH{-!W@{&yB+VgTk!+zO%tNmwX8^&xI{N zfq~YNkf5y{yM0qsNFEf}UaXJnAiqs%B#MMApA(fPqHab;5UtiB;|x~kI0Xiy{KkOs zG$xcia5N-2_-kU6IH;RhBUU#k^n+NK`m>CYs|gB0n`LjMY(Jx)nAcDI8U5^*9r&a< zi6~CA{^!g%JHoNrL0fhd=wggw>(8=#ZjHyV#*}@i#G>>Q$Q7iZW6(Bex2(}dHfSHk zZq)f-|<@azu+QVuWZKshp(YR!n6r}#D0s?-bCD|GU>BNCS z(VDTftBridG;4_st+P6muqb$|o~0alyFgY-K?;_f9I!@gWDOWA<=F3_(Tq||h_M&M z1{M>Z6=Q^MZ3Zs}GO_z9>p)gt*3S>)j=zE$;?>-K0^3+p0m zZe<5H21e!4mtT6RJE%LN@|npoz?BMfoGX823mPab)T+ubsUsX!Lt0pUQ!nd2Pc{s! zdMwE*LSy->Si24{u@L6 z5lHZT>$ZFcL&9r>(gzV*;-*+LQ8_d9oevitV#Pxjs3_t{gKoqczA_Fd|} za`^J$D}9&yuDx)trv1C-w%Oi{yYa5O@m9;C$ja6|EMaBdixn3t7Tm8bxNGtEu+(1W z0<^ko@0Qnp?Oyl2r~2aC7v8=)mGN}k^>h%W+;egN#r?DU7j0t2DPh4|bH#PpmGQQv zy=^xJeiOPG`t;=7@uB&rhwpn&FFBZd(+_w}O~XSbp!oxDUB=s#_BPFJ?)kDg<2`xT zd-BhVB31pL`W%-0tIj=L9m41PJ8SXy{rac69PEx@NBNGUylaPe$Gg6(O}w+EgeYwe zqU;b+US$H2`9Mgy{0n4j0X)nY$b@O)tTJcXAuSg0)6sJD_lpXt3&UI?Bjxdm;22t| zq%2HJqlpZKrt3S&cxbx0>vs3Y`#I%Ng+LI91-O^(&|74|^^^W8NP~g}8GwD;=qZ8+ zw9Hp5W{+Z!+Fx2Q9De4FC=QD91{!gQ*2+^c2n|BvSRe|>h259ez6jt2p=fgL$(|_= zp@XXqgk03=t{}G?f6P74b2Hi`;Kc~{5~LV8D%`yd^i989Tmk`0LHU=HGNK0*2q5!{ zA`@mm(~v@>fZ(VNRg;Jp#1c|Z=k$TUd_ccbFM|aXmCuKgqXs6G@Hwc;PHeqGc%VFs z4A4VBsE{7=+c_GU;1i3U*BwS5VjjXE1A)pY0KqGCsrXC-*%q8(Q|J!Vsa$t4aUpT_ zKV9R$;h(SRnC)Kh)ZFz*5bY}!mn#-(WjxN9GlyCdTu0k&;7>N)pZMimA5Xub@kv^9%;@gJ!+#7ka7{; zgTl`j;UIY_BKJe=h6Tq3xf_ES(rhn~X5D@!nT$VPriUR;n~Zkc(x(JRv7jgF~rxR^Vru2?_&<; znl{{-Dfl+hw7*DLsyMF?0LDA9B|NtCh!`oHKe|w_8bY+i5F&;2h~89uNa$`$!Z>s6 zMO*HjfW-?W=)tB2{=)T~-bUf|c@U^GKX~~myB4IMTCN@wAC^Za2F~MLYEw1tubMoqZ-}WOtkTd8DtEw*5L{H>z6PhS*&~kks7H~3@he$% z-IccY+hz|ec&gr=z8d`SwGUpqx9x@B4bSkOPsy$X_L zB|(Z{7N5hcm1fvvh9rsoq#p}X&hi?{a{MYuf-ZoYU&JaKv{~}d$c{ktNYVzeWqx7$ zSyL`@vD~a@mWSkYC6J5ZMg#s*U7U`kIR6#p&XRB{B`2b$4Wm~~Ci1#-lZNxN0mE5u zY9m=HtD0tGRQirJcy{eB{B(oZL|8k%jk6UGq~>c~nU;=pOUJF~C-K|y`_jR*yMDG8 z=FPi*b5M4x|{PA2ogKjxD#NqHQF=XV1-~l(};KzG8#h7$HL}!`#G~Xp2 z??9$TB$M7}$z>KssU*U@GwI3RCR-yH-lWSu zZYdCw2Kul!1bDiG_tss5;rOJ?BL^l1a>g=1(1<*8TVPGQIBSy6U{sYb2k>SW%X|yf zb(!ksbaiv4+Mli_E4aF2&fW3D1J4F{xn(}Rx(+UFneR_3#gKlrv4Q=4d7Joap-o)n zrkdN!mdU>VIdB%V1x0dFAwCqH)XghNy2vyml>;C@o-^J-E43LJ$G0aPOBYUN zQMfDyh{d9Wh_{&{nX+S~EKp)%W-9uAyVZ;0illBSnFh)6@6_*-5wa)R{5K>Rhc=A{L|6$c88g&r|W8 zEPg7W)?58@Pb<=hONxBMi~=PEDLczIxG87@pM=3DxEC#U2F2&l-&8OtehxaUlgZcj zsgl7GBl>#;ZZpTNECS`m=qJ`MNO&_vYzoqe!JuHR)p{S!C1RWl;TZiRUP~r4Prx~d zCgI!=-Ba<@kAO;H3fcy;(R;$gqjMG_7L2x;MoX1GW;F5S32UAP!lp|1mmc|q_>M}s z=&#=%_JR#Nqfu#MGOoo^_{Jh%vnPu{tpvYKnZzy9giO~-=YhTyhvnVqUWt4<%h{6# zvRKvlsmZ2bMs2MyQ$JVA1N}^`4C- zCt=ZN?|sMdr3y`lsu7$H7(IR@G98e3*oIb)F!egaEII*J;ISU%0F`*4hoq#ykXN=6 zZ5L%E!wNyDLYEoJ9?HnSS4cf86g>4ijhN6GlywK+v79lbi*hWg5G*SPk*S2=My%S3 z71g0fu?s*wu!eE9M*DYY_-U{g^re?oRnI!U+0aaOlrK}?macER=V|-e>sxSpGwym= zSQ&S7+TDDu_ntejc8uwJxA)@F3r7*YPP-egIqtc)eCMgTFnP81!%ZJ-y7%t-}Z&A>ChJOi&lYhfB8$b zjxRdazvOlNi`RkjD!YVCK&bSeazCm(jdnlrUi224=3)$`4*wEEb}7mqIg1&scrvXc z_wo#W&a7c9WYr3knyLK2c7-XY{P=x(wmJ{EY0W$VxbbGgKt1ZSz!>J~uH#xVWqw~q zl3(zJ8R1bB_gHlCy$W$)m)dedjED?*<0{S4y#2Q0E2fmbX$=vhO1^W167W>YJ*zmm zHlLBFQ5YJ3kvu&h@X)+X?644GI)aA(u%>Tx-aO;-cP zy(Cu-{M5L@;Hrb1eq%q$CN)+>c8TuXV|<{uPtov`xKiy#24)e2C8tBMueh18W1emQ zhE;yce(_&y=XdPIZ`q-LcWt`bGw*7hDScQX2nU2qy#OUVSDdG9f`1vG90|t8{XdCQ BcS`^O literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/underlyings.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/underlyings.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c963e0bfb7d4e6d683d299d3d06612f8250b946 GIT binary patch literal 85578 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/vtherm_central_api.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/vtherm_central_api.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d827d4945cbdfab6a7a96ed966c370d46099c12 GIT binary patch literal 15717 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 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/vtherm_hvac_mode.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/vtherm_hvac_mode.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2dc0d6dfed0d6c63397eee1cab64d5b62eb5c224 GIT binary patch literal 7592 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 P2g~=nlMj9soWlAaHc3i6 literal 0 HcmV?d00001 diff --git a/custom_components/versatile_thermostat/__pycache__/vtherm_state.cpython-314.pyc b/custom_components/versatile_thermostat/__pycache__/vtherm_state.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc7e3bd95d9fd595b712f1aecf38c50ca350afa8 GIT binary patch literal 11631 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 bool | None: + """Check auto-start/stop state should be triggered + Return True if the device should be turned off, False if the device should not be turned off + """ + + if self._level == AUTO_START_STOP_LEVEL_NONE: + _LOGGER.debug( + "%s - auto-start/stop is disabled", + self, + ) + self._last_should_be_off = False + return self._last_should_be_off + + _LOGGER.debug( + "%s - calculate_action: requested_hvac_mode=%s, target_temp=%s, current_temp=%s, slope_min=%s at %s", + self, + requested_hvac_mode, + target_temp, + current_temp, + slope_min, + now, + ) + + if requested_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, + ) + self._last_should_be_off = False + return self._last_should_be_off + + # 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 self._last_should_be_off + 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 + previous_should_be_off = self._last_should_be_off + if requested_hvac_mode == VThermHvacMode_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 - auto-start/stop: We need to stop, there is no need for heating for a long time.", + self, + ) + + self._last_should_be_off = True + elif temp_at_dt <= target_temp - TEMP_HYSTERESIS and nb_minutes_since_last_switch >= self._dt: + _LOGGER.info( + "%s - auto-start/stop: We need to start heating.", + self, + ) + self._last_should_be_off = False + + elif requested_hvac_mode == VThermHvacMode_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_should_be_off = True + + elif temp_at_dt >= target_temp + TEMP_HYSTERESIS and nb_minutes_since_last_switch >= self._dt: + _LOGGER.info( + "%s - We need to start cooling.", + self, + ) + self._last_should_be_off = False + + _LOGGER.debug("%s - nothing to do, requested hvac_mode is %s", self, requested_hvac_mode) + if previous_should_be_off != self._last_should_be_off: + self._last_switch_date = now + + return self._last_should_be_off + + # def calculate_action( + # self, + # hvac_mode: VThermHvacMode | None, + # saved_hvac_mode: VThermHvacMode | 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""" + + # # Check to turn-off + # # When we hit the threshold, that mean we can turn off + # if hvac_mode == VThermHvacMode_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 == VThermHvacMode_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 == VThermHvacMode_OFF and saved_hvac_mode == VThermHvacMode_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 == VThermHvacMode_OFF and saved_hvac_mode == VThermHvacMode_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) + + def reset_switch_delay(self): + """Reset the switch delay to allow immediate restart. + Should be called when target temperature changes significantly (preset change, manual temp change). + This prevents the VTherm from staying off when the user explicitly requests a higher temperature. + """ + _LOGGER.debug("%s - Resetting switch delay to allow immediate start/stop", self) + self._last_switch_date = None + # Also reset _last_calculation_date to allow immediate recalculation + # This is needed because the check for too-rapid calculations would otherwise + # ignore the next call if it happens within 24 seconds + self._last_calculation_date = None + # Reset the last decision so a user-initiated preset/temperature change always exits + # the auto-stop state. Without this, if neither the "turn on" nor the "turn off" + # condition is met (e.g. room temp near target with a slightly positive slope), + # _last_should_be_off would stay True and the VTherm would remain stopped even though + # the user explicitly asked for a different preset. + self._last_should_be_off = False + + @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}" diff --git a/custom_components/versatile_thermostat/auto_tpi_manager.py b/custom_components/versatile_thermostat/auto_tpi_manager.py new file mode 100644 index 0000000..8f6cb68 --- /dev/null +++ b/custom_components/versatile_thermostat/auto_tpi_manager.py @@ -0,0 +1,3199 @@ +"""Auto TPI Manager implementing TPI algorithm.""" + +import logging +from vtherm_api.log_collector import get_vtherm_logger +import json +import os +import math +import statistics +from datetime import datetime, timedelta +from typing import Optional +from homeassistant.util.unit_conversion import TemperatureConverter +from homeassistant.const import UnitOfTemperature +from dataclasses import dataclass, asdict, field + +import asyncio +from typing import Callable + +from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers import entity_platform, service, translation +from homeassistant.helpers.storage import Store +from homeassistant.components.recorder import history, get_instance +from homeassistant.util import dt as dt_util +from functools import partial + +from .const import ( + DOMAIN, + CONF_TPI_COEF_INT, + CONF_TPI_COEF_EXT, + CONF_AUTO_TPI_HEATING_POWER, + CONF_AUTO_TPI_COOLING_POWER, +) +from .vtherm_central_api import VersatileThermostatAPI + +_LOGGER = get_vtherm_logger(__name__) + +STORAGE_VERSION = 8 +STORAGE_KEY_PREFIX = "versatile_thermostat.auto_tpi" + +# Configurable constants for learning algorithm behavior +MIN_KINT = 0.01 # Minimum Kint threshold to maintain temperature responsiveness +OVERSHOOT_THRESHOLD = 0.2 # Temperature overshoot threshold (°C) to trigger aggressive Kext correction +OVERSHOOT_POWER_THRESHOLD = 0.05 # Minimum power (5%) to consider overshoot as Kext error +OVERSHOOT_CORRECTION_BOOST = 2.0 # Multiplier for alpha during overshoot correction +NATURAL_RECOVERY_POWER_THRESHOLD = 0.10 # Max power (10%) to consider temperature change as natural recovery +KEXT_LEARNING_MAX_GAP = 1.0 # Max gap (°C) to allow Kext learning (Near-Field vs Far-Field) +INSUFFICIENT_RISE_GAP_THRESHOLD = KEXT_LEARNING_MAX_GAP # Min gap (°C) to trigger Kint correction when temp stagnates +INSUFFICIENT_RISE_BOOST_FACTOR = 1.08 # Kint increase factor (8%) per stagnating cycle +MAX_CONSECUTIVE_KINT_BOOSTS = 5 # Max consecutive Kint boosts before warning (undersized heating) +MIN_PRE_BOOTSTRAP_CALIBRATION_RELIABILITY = 20.0 # Min reliability (%) to use calibration instead of bootstrap +MIN_EFFICIENCY_FOR_CAPACITY = 0.60 # Min efficiency (60%) to learn capacity - prevents outliers from external factors + + +@dataclass +class AutoTpiState: + """Persistent state for Auto TPI algorithm.""" + + # Learning coefficients (heat) + coeff_indoor_heat: float = 0.1 + coeff_outdoor_heat: float = 0.01 + coeff_indoor_autolearn: int = 1 # Counter + coeff_outdoor_autolearn: int = 0 + + # Learning coefficients for Cool + coeff_indoor_cool: float = 0.1 + coeff_outdoor_cool: float = 0.01 + coeff_indoor_cool_autolearn: int = 1 + coeff_outdoor_cool_autolearn: int = 0 + + # Max Capacity (physical power of the system) + max_capacity_heat: float = 0.0 + max_capacity_cool: float = 0.0 + + # Offsets. + offset: float = 0.0 + + # Previous cycle state (Snapshot for learning) + last_power: float = 0.0 + last_order: float = 0.0 + last_temp_in: float = 0.0 + last_temp_out: float = 0.0 + last_state: str = "stop" # 'heat', 'cool', 'stop' + previous_state: str = "stop" # State of the previous cycle + last_on_temp_in: float = 0.0 # Temp at the end of ON time + last_update_date: Optional[datetime] = None + last_heater_stop_time: Optional[datetime] = None # When heater stopped + + # Cycle management + cycle_start_date: Optional[datetime] = None # Start of current cycle + cycle_active: bool = False + current_cycle_cold_factor: float = 0.0 # 1.0 = cold, 0.0 = hot + current_cycle_params: dict = None # Parameters of the current/last cycle + + # Management + consecutive_failures: int = 0 + autolearn_enabled: bool = False + last_learning_status: str = "startup" + total_cycles: int = 0 # Total number of TPI cycles + consecutive_boosts: int = 0 # Track consecutive boost attempts + recent_errors: list = field(default_factory=list) # Store last N errors for regime change detection + regime_change_detected: bool = False # Flag for temporary alpha boost + learning_start_date: Optional[datetime] = None # Date when learning started + + # Capacity learning (Heat only) + capacity_heat_learn_count: int = 0 + bootstrap_failure_count: int = 0 # Number of consecutive failures to learn capacity during bootstrap + # Bootstrap is implied when capacity_heat_learn_count < 3 + + # Optional features configuration + allow_kint_boost: bool = False + allow_kext_overshoot: bool = False + + def to_dict(self): + """Convert to a JSON-safe dict for HA state attributes and storage.""" + def make_json_safe(value): + """Convert non-JSON-serializable types to JSON-safe equivalents.""" + if value is None: + return None + if isinstance(value, datetime): + return value.isoformat() + if isinstance(value, (list, tuple)): + return [make_json_safe(v) for v in value] + if isinstance(value, dict): + return {k: make_json_safe(v) for k, v in value.items()} + return value + + result = asdict(self) + return {k: make_json_safe(v) for k, v in result.items()} + + @classmethod + def from_dict(cls, data): + d = data.copy() + # Date conversion from ISO format + for date_field in ["last_update_date", "cycle_start_date", "last_heater_stop_time", "learning_start_date"]: + if d.get(date_field): + try: + # Use dt_util.parse_datetime to preserve timezone information + parsed = dt_util.parse_datetime(d[date_field]) + d[date_field] = parsed if parsed else datetime.fromisoformat(d[date_field]) + except (ValueError, TypeError): + d[date_field] = None + + # Create instance with defaults first + instance = cls() + + # Filter unknown fields and update only valid ones + valid_fields = {k for k in cls.__annotations__} + for key, value in d.items(): + if key in valid_fields: + setattr(instance, key, value) + + return instance + + +class AutoTpiManager: + """Auto TPI Manager implementing TPI algorithm.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + unique_id: str, + name: str, + cycle_min: int, + tpi_threshold_low: float = 0.0, + tpi_threshold_high: float = 0.0, + minimal_deactivation_delay: int = 0, + coef_int: float = 0.6, + coef_ext: float = 0.04, + heater_heating_time: int = 5, + heater_cooling_time: int = 5, + calculation_method: str = "ema", + heating_rate: float = 1.0, + cooling_rate: float = 1.0, + avg_initial_weight: int = 1, + ema_alpha: float = 0.15, + ema_decay_rate: float = 0.08, + aggressiveness: float = 0.9, + continuous_kext: bool = False, + continuous_kext_alpha: float = 0.04, + ): + self._hass = hass + self._name = name + self._cycle_min = cycle_min + self._config_entry = config_entry + self._enable_update_config = True + self._enable_notification = True + self._unique_id = unique_id + self._entity_id: str | None = None # Set by thermostat after entity registration + self._tpi_threshold_low = tpi_threshold_low + self._tpi_threshold_high = tpi_threshold_high + self._minimal_deactivation_delay_sec = minimal_deactivation_delay + self._heater_heating_time = heater_heating_time + self._heater_cooling_time = heater_cooling_time + + self._continuous_kext = continuous_kext + self._continuous_kext_alpha = continuous_kext_alpha + + self._temp_unit = self._hass.config.units.temperature_unit + self._unit_factor = 1.8 if self._temp_unit == UnitOfTemperature.FAHRENHEIT else 1.0 + + self._calculation_method = calculation_method + self._ema_alpha = ema_alpha + self._avg_initial_weight = avg_initial_weight + self._max_coef_int = 1.0 + # Convert rates to Celsius/h if needed + self._heating_rate = heating_rate / self._unit_factor + self._cooling_rate = cooling_rate / self._unit_factor + self._ema_decay_rate = ema_decay_rate + self._continuous_learning = False + self._keep_ext_learning = True + self._aggressiveness = aggressiveness + + # Notification management + self._last_notified_coef_int: Optional[float] = None + self._last_notified_coef_ext: Optional[float] = None + + storage_key = f"{STORAGE_KEY_PREFIX}.{unique_id.replace('.', '_')}" + self._store = Store(hass, STORAGE_VERSION, storage_key) + # Convert config coefficients (User Unit) to Internal (Celsius) + # K_C = K_F * 1.8 + self._default_coef_int = (coef_int if coef_int is not None else 0.6) * self._unit_factor + self._default_coef_ext = (coef_ext if coef_ext is not None else 0.04) * self._unit_factor + + self.state = AutoTpiState( + coeff_indoor_heat=self._default_coef_int, coeff_outdoor_heat=self._default_coef_ext, coeff_indoor_cool=self._default_coef_int, coeff_outdoor_cool=self._default_coef_ext + ) + self._calculated_params = {} + + # Transient state + self._current_temp_in: float = 0.0 + self._current_temp_out: float = 0.0 + self._current_target_temp: float = 0.0 + self._current_hvac_mode: str = "heat" # 'heat' or 'cool' (or 'off' etc) + self._last_cycle_power_efficiency: float = 1.0 + self._save_lock = asyncio.Lock() + + self._timer_capture_remove_callback: Callable[[], None] | None = None + self._learning_just_completed: bool = False # Transient flag to suppress 'cycle interrupted' log after learning + + # Interruption management + self._current_cycle_interrupted: bool = False + self._central_boiler_off: bool = False + self._current_is_heating_failure: bool = False + + # Shutdown safety check + self._is_vtherm_stopping_callback: Callable[[], bool] | None = None + + def get_filtered_state(self) -> dict: + """Get the AutoTpiState as a dict, but filtered for public exposure.""" + data = self.state.to_dict() + + # 1. Remove internal debugging attributes + if "recent_errors" in data: + del data["recent_errors"] + + # 2. Filter counters if learning session is NOT active (Continuous Kext only) + if not self.state.autolearn_enabled: + # If main learning is off, hide indoor counters as they are not updated/relevant in continuous mode + # (Continuous mode only updates Kext/Outdoor) + keys_to_hide = [ + "coeff_indoor_autolearn", + "coeff_indoor_cool_autolearn", + "learning_start_date", + "last_learning_status" + ] + for k in keys_to_hide: + if k in data: + del data[k] + + # 2. Filter based on Mode + is_cool_mode = self._current_hvac_mode == "cool" + + keys_to_remove = [] + for key in data.keys(): + if is_cool_mode: + # In Cool mode, remove heat-related keys + if "_heat" in key: + keys_to_remove.append(key) + else: + # In Heat mode, remove cool-related keys + if "_cool" in key: + keys_to_remove.append(key) + + for key in keys_to_remove: + del data[key] + + return data + + def set_is_vtherm_stopping_callback(self, callback: Callable[[], bool]): + """Set a callback to check if the VTherm is stopping.""" + self._is_vtherm_stopping_callback = callback + + + def _to_celsius(self, temp: float) -> float: + """Convert temperature to Celsius if needed.""" + if temp is None: + return 0.0 + if self._temp_unit == UnitOfTemperature.FAHRENHEIT: + return TemperatureConverter.convert(temp, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS) + return temp + + async def async_update_learning_data(self, coef_int: float = None, coef_ext: float = None, capacity: float = None, is_heat_mode: bool = True): + """Update coefficients and/or capacity in one go to avoid double reload.""" + + updates = {} + + # 1. Update coefficients if provided + if coef_int is not None: + updates[CONF_TPI_COEF_INT] = round(coef_int / self._unit_factor, 3) + + if coef_ext is not None: + updates[CONF_TPI_COEF_EXT] = round(coef_ext / self._unit_factor, 3) + + # 2. Update capacity if provided and valid + if capacity is not None and capacity > 0.0: + rate_key = CONF_AUTO_TPI_HEATING_POWER if is_heat_mode else CONF_AUTO_TPI_COOLING_POWER + updates[rate_key] = round(capacity * self._unit_factor, 3) + + # Also update local state (Memory) + if is_heat_mode: + self.state.max_capacity_heat = capacity + self._heating_rate = capacity + else: + self.state.max_capacity_cool = capacity + self._cooling_rate = capacity + + # 3. Always save to local storage (Persistence) + await self.async_save_data() + + # 4. Check if we should skip config update (Restart/Shutdown safety) + if not self._enable_update_config: + _LOGGER.debug("%s - Auto TPI: update_learning_data - enable_update_config is False", self._name) + return + + if self._is_vtherm_stopping_callback and self._is_vtherm_stopping_callback(): + _LOGGER.debug("%s - Auto TPI: update_learning_data - VTherm is stopping, skipping config update", self._name) + return + + if not updates: + _LOGGER.debug("%s - Auto TPI: update_learning_data - no updates to apply", self._name) + return + + # 5. Apply atomic config update (no reload: flag prevents update_listener from reloading) + api = VersatileThermostatAPI.get_vtherm_api(self._hass) + if api: + api.skip_reload_on_config_update = True + try: + new_data = {**self._config_entry.data, **updates} + self._hass.config_entries.async_update_entry(self._config_entry, data=new_data) + finally: + if api: + api.skip_reload_on_config_update = False + + _LOGGER.info( + "%s - Auto TPI: ATOMIC UPDATE: Kint=%s, Kext=%s, Capacity=%s", + self._name, + f"{coef_int:.3f}" if coef_int is not None else "N/A", + f"{coef_ext:.3f}" if coef_ext is not None else "N/A", + f"{capacity:.3f}" if capacity is not None else "N/A" + ) + + + + + + + + async def process_learning_completion(self) -> Optional[dict]: + """ + Processes the learned coefficients after a cycle to: + 1. Check if learning is finished/stabilized. + 2. Apply continuous learning if enabled. + 3. Persist coefficients to HA config if enabled. + 4. Send persistent notifications if enabled. + + Returns a dict of finalized coefficients if persisted, or None. + """ + + is_cool_mode = self._current_hvac_mode == "cool" + + if is_cool_mode: + k_int = self.state.coeff_indoor_cool + k_ext = self.state.coeff_outdoor_cool + int_cycles_count = self.state.coeff_indoor_cool_autolearn + ext_cycles_count = self.state.coeff_outdoor_cool_autolearn + else: + k_int = self.state.coeff_indoor_heat + k_ext = self.state.coeff_outdoor_heat + int_cycles_count = self.state.coeff_indoor_autolearn + ext_cycles_count = self.state.coeff_outdoor_autolearn + + # 1. Check if learning is finished/stabilized (for non-continuous learning) + # We check if the *raw* counter has reached the threshold, which accounts for _avg_initial_weight. + INT_CYCLES_THRESHOLD = 50 + self._avg_initial_weight + EXT_CYCLES_THRESHOLD = 50 + + is_int_finished = int_cycles_count >= INT_CYCLES_THRESHOLD + is_kext_standard_finished = ext_cycles_count >= EXT_CYCLES_THRESHOLD + + is_ext_finished = is_kext_standard_finished and (not self._keep_ext_learning or is_int_finished) + + if self._continuous_learning: + # For continuous learning, we persist if the base threshold is met (stabilized). + if is_int_finished: + _LOGGER.debug("%s - Auto TPI: Continuous learning stabilized (Kint > %d cycles). Persisting values.", self._name, INT_CYCLES_THRESHOLD - self._avg_initial_weight) + else: + _LOGGER.debug("%s - Auto TPI: Continuous learning in progress (Kint cycles: %d). Skipping persistence.", self._name, int_cycles_count - self._avg_initial_weight) + return None + else: + # Standard learning: stop if both finished. + if not (is_int_finished and is_ext_finished): + _LOGGER.debug( + "%s - Auto TPI: Learning in progress (Kint cycles: %d/%d, Kext cycles: %d/%d). Skipping persistence.", + self._name, + int_cycles_count - self._avg_initial_weight, + INT_CYCLES_THRESHOLD - self._avg_initial_weight, + ext_cycles_count, + EXT_CYCLES_THRESHOLD, + ) + return None + else: + _LOGGER.info("%s - Auto TPI: Learning completed. Persisting final coefficients and stopping learning.", self._name) + # Calling stop_learning will NOT save capacity immediately (we do it in atomic update below) + await self.stop_learning(save_capacity=False) + + # Check if we also need to update capacity (if it was learned) + # This combines both updates into ONE config entry update + capacity_to_save = None + if self.state.capacity_heat_learn_count >= 3: + # Check if value has changed + current_conv_capacity = self._config_entry.data.get(CONF_AUTO_TPI_HEATING_POWER) + current_capacity = current_conv_capacity / self._unit_factor if current_conv_capacity else 0.0 + + if abs(current_capacity - self.state.max_capacity_heat) > 0.01: + capacity_to_save = self.state.max_capacity_heat + + await self.async_update_learning_data(coef_int=k_int, coef_ext=k_ext, capacity=capacity_to_save, is_heat_mode=True) + + + + # 4. Send persistent notifications if enabled + if self._enable_notification: + # Implement "notify once" logic + # Check for a significant change (> 0.005) or if it's the first time + if (self._last_notified_coef_int is None or abs(self._last_notified_coef_int - k_int) > 0.005) or ( + self._last_notified_coef_ext is None or abs(self._last_notified_coef_ext - k_ext) > 0.005 + ): + + # Get translated message. Since I cannot read translations, I will use a simple English message. + title = f"Versatile Thermostat: Auto TPI Learned Coefficients for {self._name}" + message = f"Auto TPI has learned new coefficients: Indoor={round(k_int, 3)}, Outdoor={round(k_ext, 3)} (Cycles: Int={int_cycles_count - self._avg_initial_weight}, Ext={ext_cycles_count}). These values have been saved to the configuration." + + try: + await self._hass.services.async_call( + "persistent_notification", + "create", + { + "title": title, + "message": message, + "notification_id": f"autotpi_learning_completed_{self._unique_id}", + }, + blocking=False, + ) + self._last_notified_coef_int = k_int + self._last_notified_coef_ext = k_ext + _LOGGER.info("%s - Auto TPI: Persistent notification sent for final coefficients.", self._name) + except Exception as e: + _LOGGER.error("%s - Auto TPI: Error sending persistent notification: %s", self._name, e) + + # Return learned coefficients converted back to User Unit + # Internal: C. Output: F/C. If F, output = internal / 1.8 (internal * 0.55 ?) + # K_F = K_C / 1.8. + return { + CONF_TPI_COEF_INT: k_int / self._unit_factor, + CONF_TPI_COEF_EXT: k_ext / self._unit_factor + } + + async def async_save_data(self): + """Save data.""" + await self._store.async_save(self.state.to_dict()) + + async def async_load_data(self): + """Load data.""" + data = await self._store.async_load() + + if not data: + # Try to migrate from old JSON file + old_storage_key = f"versatile_thermostat_{self._unique_id}_auto_tpi_v2.json" + old_path = self._hass.config.path(f".storage/{old_storage_key}") + if os.path.exists(old_path): + _LOGGER.debug("%s - Auto TPI: Migrating from old storage %s", self._name, old_path) + try: + with open(old_path, "r") as f: + old_json = json.load(f) + # Extract state from old format + data = old_json.get("state", old_json) + await self._store.async_save(data) # Save to new format + os.remove(old_path) # Clean up old file + except Exception as e: + _LOGGER.error("%s - Auto TPI: Migration error: %s", self._name, e) + + if data: + self.state = AutoTpiState.from_dict(data) + + # Clamping: Apply new max_coef_int to loaded coefficients immediately, + # in case the user lowered the limit via config flow. + self.state.coeff_indoor_heat = min(self.state.coeff_indoor_heat, self._max_coef_int) + self.state.coeff_indoor_cool = min(self.state.coeff_indoor_cool, self._max_coef_int) + + # Capacity update logic on load (to fix startup issue after config change) + # We prioritize the capacity from the latest config flow over the persisted state + # if they are different and the config value is valid. This handles both an initial + # state with 0 capacity and a configuration change on restart. + + is_capacity_heat_outdated = self.state.max_capacity_heat != self._heating_rate + is_capacity_cool_outdated = self.state.max_capacity_cool != self._cooling_rate + + # Handle capacity reset: if configured heat_rate is 0, reset capacity to trigger bootstrap + if self._heating_rate == 0.0 and self.state.max_capacity_heat > 0.0: + _LOGGER.info( + "%s - Auto TPI: Configured heat_rate is 0, resetting capacity (was %.3f) to trigger bootstrap.", + self._name, + self.state.max_capacity_heat, + ) + self.state.max_capacity_heat = 0.0 + self.state.capacity_heat_learn_count = 0 + elif is_capacity_heat_outdated and self._heating_rate > 0.0: + if self.state.max_capacity_heat == 0.0: + _LOGGER.info( + "%s - Auto TPI: Overwriting persisted max_capacity_heat (0.000) with new configured value (%.3f) on load.", + self._name, + self._heating_rate, + ) + self.state.max_capacity_heat = self._heating_rate + self.state.capacity_heat_learn_count = 3 # Assume learned if we take config value + else: + _LOGGER.info( + "%s - Auto TPI: Persisted max_capacity_heat (%.3f) differs from config (%.3f). Keeping persisted value.", + self._name, + self.state.max_capacity_heat, + self._heating_rate, + ) + # Sync the effective rate to the persisted one + self._heating_rate = self.state.max_capacity_heat + + # Handle capacity reset for cooling mode + if self._cooling_rate == 0.0 and self.state.max_capacity_cool > 0.0: + _LOGGER.info( + "%s - Auto TPI: Configured cooling_rate is 0, resetting capacity (was %.3f) to trigger bootstrap.", + self._name, + self.state.max_capacity_cool, + ) + self.state.max_capacity_cool = 0.0 + # Note: capacity_cool_learn_count does not exist, cooling bootstrap uses different logic + elif is_capacity_cool_outdated and self._cooling_rate > 0.0: + # Same logic for cooling capacity + if self.state.max_capacity_cool == 0.0: + _LOGGER.info( + "%s - Auto TPI: Overwriting persisted max_capacity_cool (0.000) with new configured value (%.3f) on load.", + self._name, + self._cooling_rate, + ) + self.state.max_capacity_cool = self._cooling_rate + else: + _LOGGER.info( + "%s - Auto TPI: Persisted max_capacity_cool (%.3f) differs from config (%.3f). Keeping persisted value.", + self._name, + self.state.max_capacity_cool, + self._cooling_rate, + ) + # Sync the effective rate to the persisted one + self._cooling_rate = self.state.max_capacity_cool + + if is_capacity_heat_outdated or is_capacity_cool_outdated: + await self.async_save_data() # Save the new correct config value + + # If no learning has been done yet, force the configured defaults + if self.state.total_cycles == 0: + _LOGGER.info("%s - Auto TPI: No learning cycles yet. Enforcing configured coefficients.", self._name) + self.state.coeff_indoor_heat = self._default_coef_int + self.state.coeff_outdoor_heat = self._default_coef_ext + self.state.coeff_indoor_cool = self._default_coef_int + self.state.coeff_outdoor_cool = self._default_coef_ext + # Initialize counters with the configured weight + self.state.coeff_indoor_autolearn = self._avg_initial_weight + self.state.coeff_indoor_cool_autolearn = self._avg_initial_weight + self.state.coeff_outdoor_autolearn = 0 + self.state.coeff_outdoor_cool_autolearn = 0 + + _LOGGER.info("%s - Auto TPI: State loaded. Cycles: %d, Indoor learn count: %d", self._name, self.state.total_cycles, self.state.coeff_indoor_autolearn) + else: + self.state = AutoTpiState( + coeff_indoor_heat=self._default_coef_int, + coeff_outdoor_heat=self._default_coef_ext, + coeff_indoor_cool=self._default_coef_int, + coeff_outdoor_cool=self._default_coef_ext, + ) + + # Reset last_learning_status on load to avoid stale messages from previous sessions + self.state.last_learning_status = "learning_started" + + # Reset cycle state to discard any cycle interrupted by the reboot + # This prevents "cycle_gap_detected" or validation failures on the first cycle after restart + self.state.cycle_active = False + self.state.cycle_start_date = None + + # MIGRATION FIX: If capacity is already known (legacy or manual), mark as learned + # This prevents re-triggering bootstrap (count=0) for existing users + if self.state.max_capacity_heat > 0 and self.state.capacity_heat_learn_count == 0: + _LOGGER.info("%s - Auto TPI: Existing capacity found (%.3f), marking as learned (count=3)", self._name, self.state.max_capacity_heat) + self.state.capacity_heat_learn_count = 3 + + await self.calculate() + + async def update(self, room_temp: float, ext_temp: float, hvac_mode: str, target_temp: float, is_overpowering_detected: bool = False, is_central_boiler_off: bool = False, is_heating_failure: bool = False) -> float: + """Update state with new data. + + This method is called at each control_heating cycle. + It updates the transient state used for power calculation and future learning. + + Returns the calculated power for validation/indication. + """ + + # Check for Overpowering Interruption + # If overpowering is detected, the heating/cooling is artificially stopped/limited. + # We must mark this cycle as interrupted so we don't learn from it (false data). + if is_overpowering_detected: + if not self._current_cycle_interrupted: + _LOGGER.info("%s - Auto TPI: Cycle interrupted by Overpowering/Power Shedding. Learning will be skipped for this cycle.", self._name) + self._current_cycle_interrupted = True + + # Store current values for later use in cycle callbacks + # Convert inputs to Celsius for internal logic + self._current_temp_in = self._to_celsius(room_temp) if room_temp is not None else 0.0 + self._current_temp_out = self._to_celsius(ext_temp) if ext_temp is not None else 0.0 + self._current_target_temp = self._to_celsius(target_temp) if target_temp is not None else 0.0 + self._current_hvac_mode = hvac_mode + self._central_boiler_off = is_central_boiler_off + self._current_is_heating_failure = is_heating_failure + + # Calculate and return power + # Use hvac_mode to force direction + calc_state_str = "stop" + if hvac_mode == "cool": + calc_state_str = "cool" + elif hvac_mode == "heat": + calc_state_str = "heat" + + return self.calculate_power(self._current_target_temp, self._current_temp_in, self._current_temp_out, calc_state_str) + + async def calculate(self) -> Optional[dict]: + """Calculate TPI parameters, using aggressive coefficients during bootstrap.""" + + # Determine if in bootstrap (capacity not yet learned) + in_bootstrap = ( + self.state.max_capacity_heat == 0 or + self.state.capacity_heat_learn_count < 3 + ) + + # Temporarily override learned coefficients if in bootstrap + saved_kint = self.state.coeff_indoor_heat + saved_kext = self.state.coeff_outdoor_heat + + if in_bootstrap: + # Use aggressive coefficients for bootstrap + # User requested 1.0 / 0.1 as "normal" values (sufficiently aggressive vs 0.6 default) + KINT_BOOTSTRAP = 1.0 + KEXT_BOOTSTRAP = 0.1 + + self.state.coeff_indoor_heat = KINT_BOOTSTRAP + self.state.coeff_outdoor_heat = KEXT_BOOTSTRAP + + try: + # Return current coefficients for the thermostat to use + params = {} + + # Use hvac_mode to determine which coefficients to return + # This prevents flapping when switching between heating/cooling actions while in the same mode (e.g. idle) + # Note: hvac_mode usually comes from VThermHvacMode (heat, cool, off, auto...) + + is_cool_mode = self._current_hvac_mode == "cool" + + if is_cool_mode: + params[CONF_TPI_COEF_INT] = self.state.coeff_indoor_cool / self._unit_factor + params[CONF_TPI_COEF_EXT] = self.state.coeff_outdoor_cool / self._unit_factor + else: + params[CONF_TPI_COEF_INT] = self.state.coeff_indoor_heat / self._unit_factor + params[CONF_TPI_COEF_EXT] = self.state.coeff_outdoor_heat / self._unit_factor + + self._calculated_params = params + return params + finally: + # Restore original values + if in_bootstrap: + self.state.coeff_indoor_heat = saved_kint + self.state.coeff_outdoor_heat = saved_kext + + def _get_adaptive_alpha(self, cycle_count: int) -> float: + """Calculate adaptive alpha for EMA, with temporary boost on regime change.""" + + # Standard calculation + base_alpha = self._ema_alpha / (1 + self._ema_decay_rate * cycle_count) + + # If continuous learning is enabled and regime change detected, temporary boost + if self._continuous_learning and self.state.regime_change_detected: + # Max boost alpha is min(base_alpha * 3.0, 0.15) + # We want to ensure the base alpha is not too small before boosting. + # If base_alpha is very small (after many cycles), boost will still be limited. + boost_alpha = min(base_alpha * 3.0, 0.15) + + _LOGGER.info(f"%s - Auto TPI: Regime change detected, boosting alpha: {base_alpha:.3f} -> {boost_alpha:.3f}", self._name) + + # The flag will be reset in _learn_indoor after consumption + return boost_alpha + + return base_alpha + + def _detect_regime_change(self, recent_errors: list) -> bool: + """ + Detects a thermal regime change (systematic bias). + If detected, we can temporarily increase alpha for faster adaptation. + """ + N = 10 + if not self._continuous_learning or len(recent_errors) < N: + return False + + # We only look at the last N errors + errors_to_check = recent_errors[-N:] + + # Simple statistical test: + # Do the last N errors have a systematic bias? + # mean_error is the average 'correction needed' in °C + mean_error = sum(errors_to_check) / N + + # Calculate standard deviation + # Avoid zero division + std_error = (sum((e - mean_error) ** 2 for e in errors_to_check) / N) ** 0.5 + if std_error == 0: + return False + + # Student's t-test: significant systematic error? + t_stat = abs(mean_error) / (std_error / math.sqrt(N)) + + # 95% confidence threshold (t > 2.0 for n=10) + return t_stat > 2.0 + + def _should_learn(self) -> bool: + """Check if learning should be performed.""" + # We learn if: + # 1. Main learning session is active (autolearn_enabled) + # 2. OR Continuous Kext is enabled (we will filter Kint vs Kext inside _perform_learning) + if not self.state.autolearn_enabled and not self._continuous_kext: + return False + + # Power conditions: 0 < last_power < saturation_threshold + # If power is >= saturation_threshold, the cycle is saturated and we skip learning. + saturation_threshold = self.saturation_threshold + if not (0 < self.state.last_power < saturation_threshold): + _LOGGER.debug("%s - Auto TPI: Not learning - Power out of range (%.3f not in 0 < power < %.3f)", self._name, self.state.last_power, saturation_threshold) + return False + + if self._current_cycle_interrupted: + _LOGGER.debug("%s - Auto TPI: Not learning - Cycle was interrupted (e.g. Power Shedding)", self._name) + return False + + if self._central_boiler_off: + _LOGGER.debug("%s - Auto TPI: Not learning - Central boiler is OFF although VTherm is active (boiler below activation threshold)", self._name) + return False + + if self._current_is_heating_failure: + _LOGGER.debug("%s - Auto TPI: Not learning - Heating/Cooling failure detected", self._name) + return False + + # Failures check + if self.state.consecutive_failures >= 3: + return False + + # 1. First Cycle Exclusion + if self.state.previous_state == "stop": + _LOGGER.debug("%s - Auto TPI: Not learning - First cycle (previous state was stop)", self._name) + return False + if self.state.last_order == 0: + _LOGGER.debug("%s - Auto TPI: Not learning - Last order is 0", self._name) + return False + + # 2. Mild Weather Exclusion (Safe Ratio) + # Avoid division by small numbers or learning when delta is too small to be significant + delta_out = self.state.last_order - self._current_temp_out + delta_out_threshold = 1.0 # Celsius + if abs(delta_out) < delta_out_threshold: + _LOGGER.debug("%s - Auto TPI: Not learning - Delta out too small (< %.1f)", self._name, delta_out_threshold) + return False + + # Natural drift exclusion - check temperature at CYCLE START + # If temp was already past setpoint at cycle START, this is passive drift, not active regulation + # is_heat = self.state.last_state == 'heat' + # is_cool = self.state.last_state == 'cool' + + # if is_heat and self.state.last_temp_in > self.state.last_order + 0.05: + # _LOGGER.debug("%s - Auto TPI: Not learning - Passive cooling at cycle start (T_in %.2f > Target %.2f + 0.05)", + # self._name, self.state.last_temp_in, self.state.last_order) + # return False + + # if is_cool and self.state.last_temp_in < self.state.last_order - 0.05: + # _LOGGER.debug("%s - Auto TPI: Not learning - Passive heating at cycle start (T_in %.2f < Target %.2f - 0.05)", + # self._name, self.state.last_temp_in, self.state.last_order) + # return False + + return True + + def _get_no_learn_reason(self) -> str: + """Get reason why learning is not happening.""" + if not self.state.autolearn_enabled and not self._continuous_kext: + return "learning_disabled" + + saturation_threshold = self.saturation_threshold # pylint: disable=no-member + if not (0 < self.state.last_power < saturation_threshold): + return f"power_out_of_range({self.state.last_power * 100:.1f}% vs Saturation {saturation_threshold * 100:.1f}%)" + + if self._current_cycle_interrupted: + return "cycle_interrupted_by_overpowering" + + if self._central_boiler_off: + return "central_boiler_off" + + if self._current_is_heating_failure: + return "heating_failure_detected" + + if self.state.consecutive_failures >= 3: + return f"too_many_failures({self.state.consecutive_failures})" + + if self.state.previous_state == "stop": + return "startup_cycle" + + if self.state.last_order == 0: + return "target_temp_is_zero" + + delta_out = self.state.last_order - self._current_temp_out + if abs(delta_out) < 1.0: + return f"outdoor_delta_too_small({delta_out:.1f})" + + return "unknown" + + async def _perform_learning(self, current_temp_in: float, current_temp_out: float): + """Execute the learning logic based on previous state and current observations.""" + + is_heat = self.state.last_state == "heat" + is_cool = self.state.last_state == "cool" + + if not (is_heat or is_cool): + self.state.last_learning_status = "not_heating_or_cooling" + _LOGGER.debug("%s - Auto TPI: Not learning - system was in %s mode", self._name, self.state.last_state) + return + + # Check if setpoint changed during the cycle - if so, skip ALL learning + # This prevents incorrect coefficient updates when user adjusts temperature mid-cycle + setpoint_changed = abs(self._current_target_temp - self.state.last_order) > 0.1 + if setpoint_changed: + self.state.last_learning_status = "setpoint_changed_during_cycle" + _LOGGER.debug( + "%s - Auto TPI: Skipping learning - setpoint changed during cycle (%.1f → %.1f)", + self._name, self.state.last_order, self._current_target_temp + ) + return + + target_temp = self.state.last_order + + # Calculate deltas based on direction + if is_heat: + temp_progress = current_temp_in - self.state.last_temp_in + target_diff = self.state.last_order - self.state.last_temp_in + outdoor_condition = current_temp_out < self.state.last_order + else: # Cool + temp_progress = self.state.last_temp_in - current_temp_in + target_diff = self.state.last_temp_in - self.state.last_order + outdoor_condition = current_temp_out > self.state.last_order + + # CASE 0: Overshoot Correction (BEFORE standard learning) + # ---------------------------------------------------------- + # When room is overheating despite heat still being applied, + # Kext is clearly too high. Correct it aggressively before + # attempting normal learning. + # + # IMPORTANT: Only correct if temperature is NOT FALLING despite overshoot. + # If temp is falling naturally (e.g., after setpoint was lowered), the + # system is working correctly - no need to reduce Kext. + # If temp stagnates or rises, Kext is too high (preventing natural cooling). + # A small threshold (0.02°C) filters out sensor noise. + temp_not_falling = current_temp_in >= self.state.last_temp_in - 0.02 + + if is_heat: + overshoot = current_temp_in - target_temp + if overshoot > OVERSHOOT_THRESHOLD and self.state.last_power > OVERSHOOT_POWER_THRESHOLD and temp_not_falling: + _LOGGER.info( + "%s - Auto TPI: Overshoot detected (%.2f°C > %.2f°C threshold, power=%.1f%%, temp not falling)", + self._name, overshoot, OVERSHOOT_THRESHOLD, self.state.last_power * 100 + ) + if self._correct_kext_overshoot(overshoot, is_cool=False): + return # Skip other learning for this cycle + elif is_cool: + temp_not_rising = current_temp_in <= self.state.last_temp_in + 0.02 + overshoot = target_temp - current_temp_in + if overshoot > OVERSHOOT_THRESHOLD and self.state.last_power > OVERSHOOT_POWER_THRESHOLD and temp_not_rising: + _LOGGER.info( + "%s - Auto TPI: Overcooling detected (%.2f°C > %.2f°C threshold, power=%.1f%%, temp not rising)", + self._name, overshoot, OVERSHOOT_THRESHOLD, self.state.last_power * 100 + ) + if self._correct_kext_overshoot(overshoot, is_cool=True): + return # Skip other learning for this cycle + + # CASE 0.5: Insufficient Rise Correction + # ---------------------------------------- + # When temperature stagnates despite a significant gap (> 0.3°C) + # and power is not saturated, Kint is likely too low. + # Instead of incorrectly adjusting Kext, we boost Kint. + # + # This handles the scenario where: + # - target_diff > 0.3°C (significant gap to setpoint) + # - temp_progress < 0.02 (temperature is stagnating or dropping) + # - power < 0.99 (not saturated, so we CAN increase power) + # + # In this case, standard indoor learning fails (requires temp_progress > 0.05) + # and the system incorrectly falls through to outdoor learning, increasing Kext. + + temp_stagnating = temp_progress < 0.02 + + if target_diff > INSUFFICIENT_RISE_GAP_THRESHOLD and temp_stagnating and self.state.last_power < 0.99: + if self._correct_kint_insufficient_rise(target_diff, temp_progress, is_cool): + return # Kint corrected, skip other learning for this cycle + + # CASE 1: Indoor Learning + # --------------------------- + # Strict conditions to avoid false positives: + # - Significant temperature progress (> 0.05°C) + # - Significant gap to cover (> 0.1°C) + # - Power not saturated (0 < power < 0.99) + # - Main Learning Session MUST be active (we don't learn Kint in continuous mode) + + if self.state.autolearn_enabled and 0 < self.state.last_power < 0.99: + temp_progress_threshold = 0.05 + target_diff_threshold = 0.01 + if temp_progress > temp_progress_threshold and target_diff > target_diff_threshold: + # Indoor learning attempt + error = self._learn_indoor(target_diff, temp_progress, self._last_cycle_power_efficiency, is_cool) + if error is not None: + # Learning was successful - temperature is rising + self.state.last_learning_status = f"learned_indoor_{'cool' if is_cool else 'heat'}" + _LOGGER.info("%s - Auto TPI: Indoor coefficient learned successfully (Error: %.3f)", self._name, error) + self._learning_just_completed = True + + # Reset consecutive Kint boosts counter since temperature is now rising + if self.state.consecutive_boosts > 0: + _LOGGER.debug("%s - Auto TPI: Resetting consecutive_boosts counter (was %d)", self._name, self.state.consecutive_boosts) + self.state.consecutive_boosts = 0 + + # Continuous Learning: Track error and detect regime change + if self._continuous_learning: + self.state.recent_errors.append(error) + # Keep only the last 20 errors (N=10 for detection + buffer) + if len(self.state.recent_errors) > 20: + self.state.recent_errors = self.state.recent_errors[-20:] + + is_regime_change = self._detect_regime_change(self.state.recent_errors) + if is_regime_change and not self.state.regime_change_detected: + self.state.regime_change_detected = True + _LOGGER.warning("%s - Auto TPI: SYSTEMIC REGIME CHANGE DETECTED. Alpha boost activated.", self._name) + + return # Indoor success, we exit + else: + # Indoor failed, reason already logged in _learn_indoor + _LOGGER.debug("%s - Auto TPI: Indoor learning failed, will try outdoor", self._name) + else: + _LOGGER.debug("%s - Auto TPI: Indoor conditions not met (progress=%.3f, target_diff=%.3f)", self._name, temp_progress, target_diff) + else: + _LOGGER.debug("%s - Auto TPI: Skipping indoor coeff learning because power is saturated (%.1f%%)", self._name, self.state.last_power * 100) + + # CASE 2: Outdoor Learning + # ---------------------------- + # Executed when: + # - Indoor was not applicable (conditions not met) + # - OR indoor failed (_learn_indoor returned False) + # Conditions: + # - Relevant outdoor temperature (outdoor_condition) + # - Significant remaining gap + + gap_in = target_temp - current_temp_in + gap_threshold = 0.05 + + if outdoor_condition and abs(gap_in) > gap_threshold: + # Domain Separation: Far-Field vs Near-Field + # If the gap is large (> KEXT_LEARNING_MAX_GAP), it's a transient state (Kint domain). + # Kext (Steady State) should only be learned in Near-Field. + # + # EXCEPTION: During overshoot with significant power, we MUST learn Kext regardless + # of gap size. Overshoot means temp > target (heat) or temp < target (cool), + # which is gap_in < 0 (heat) or gap_in > 0 (cool) - the opposite of normal Far-Field. + is_overshoot_heat = is_heat and gap_in < 0 + is_overshoot_cool = is_cool and gap_in > 0 + has_significant_power = self.state.last_power >= NATURAL_RECOVERY_POWER_THRESHOLD + is_active_overshoot = (is_overshoot_heat or is_overshoot_cool) and has_significant_power + + if abs(gap_in) > KEXT_LEARNING_MAX_GAP and not is_active_overshoot: + self.state.last_learning_status = f"gap_too_large_for_outdoor(gap={gap_in:.2f} > {KEXT_LEARNING_MAX_GAP})" + _LOGGER.debug( + "%s - Auto TPI: Skipping outdoor learning: Gap %.2f > %.2f - Far field stagnation is a Kint/Capacity issue, not Kext.", + self._name, abs(gap_in), KEXT_LEARNING_MAX_GAP + ) + return + + if self._learn_outdoor(current_temp_in, current_temp_out, is_cool): + if "naturally" not in self.state.last_learning_status: + self.state.last_learning_status = f"learned_outdoor_{'cool' if is_cool else 'heat'}" + _LOGGER.info("%s - Auto TPI: Outdoor coefficient learned successfully", self._name) + self._learning_just_completed = True + return # Outdoor success + else: + _LOGGER.debug("%s - Auto TPI: Outdoor learning failed", self._name) + else: + _LOGGER.debug("%s - Auto TPI: Outdoor conditions not met (outdoor_condition=%s, gap_in=%.3f)", self._name, outdoor_condition, gap_in) + + # No learning was possible + self.state.last_learning_status = f"no_learning_possible(progress={temp_progress:.2f},target_diff={target_diff:.2f},gap_in={gap_in:.2f})" + _LOGGER.debug("%s - Auto TPI: No learning possible - %s", self._name, self.state.last_learning_status) + + def _learn_indoor(self, delta_theoretical: float, delta_real: float, efficiency: float = 1.0, is_cool: bool = False) -> Optional[float]: + """Learn indoor coefficient and optionally capacity.""" + + real_rise = delta_real + rise_threshold = 0.01 + + if real_rise <= rise_threshold: + _LOGGER.debug("%s - Auto TPI: Cannot learn indoor - real_rise %.3f <= %.3f. Will try outdoor learning.", self._name, real_rise, rise_threshold) + self.state.last_learning_status = "real_rise_too_small" + return None + + + # === KINT LEARNING === + # 1. Get adiabatic capacity + ref_capacity_h = self.state.max_capacity_heat if not is_cool else self.state.max_capacity_cool + + # Fallback if not learned yet + if ref_capacity_h <= 0: + count = self.state.capacity_heat_learn_count + + if count == 0: + ref_capacity_h = 0.5 # Very conservative for first cycle + _LOGGER.warning( + "%s - First cycle: using very conservative capacity 0.5°C/h", + self._name + ) + else: + ref_capacity_h = 1.0 # Standard fallback + _LOGGER.debug( + "%s - Capacity not yet converged (count=%d), using fallback 1.0°C/h", + self._name, count + ) + + # If no capacity defined, skip learning for this cycle + if ref_capacity_h <= 0: + _LOGGER.debug("%s - Auto TPI: Cannot learn indoor - no capacity defined (ref_capacity_h=%.2f)", self._name, ref_capacity_h) + self.state.last_learning_status = "no_capacity_defined" + return False + + # 2. Calculate Effective Capacity with thermal losses + if is_cool: + k_ext = self.state.coeff_outdoor_cool + delta_t = self._current_temp_out - self._current_temp_in + else: + k_ext = self.state.coeff_outdoor_heat + delta_t = self._current_temp_in - self._current_temp_out + + loss_factor = k_ext * max(0.0, delta_t) + loss_factor = min(loss_factor, 0.95) # Prevent going negative + + effective_capacity_h = ref_capacity_h * (1.0 - loss_factor) + + # 3. Calculate Max Achievable Rise in this cycle (°C) + cycle_duration_h = self._cycle_min / 60.0 + max_achievable_rise = effective_capacity_h * cycle_duration_h * efficiency + + _LOGGER.debug( + "%s - Auto TPI: Capacity calc: ref=%.3f °C/h, loss=%.2f, eff=%.3f °C/h, max_rise=%.3f °C (cycle=%.1f min, eff=%.2f)", + self._name, + ref_capacity_h, + loss_factor, + effective_capacity_h, + max_achievable_rise, + self._cycle_min, + efficiency, + ) + + # 4. Calculate adjusted_theoretical: aim for full gap, capped by capacity + adjusted_theoretical = min(delta_theoretical, max_achievable_rise) + + if max_achievable_rise < delta_theoretical: + mode_str = "cooling" if is_cool else "heating" + _LOGGER.debug("%s - Auto TPI: Target rise clamped from %.3f to %.3f (Max %s Capacity)", self._name, delta_theoretical, max_achievable_rise, mode_str) + + if adjusted_theoretical <= 0: + _LOGGER.warning("%s - Auto TPI: Cannot learn indoor - adjusted_theoretical <= 0 (max_rise=%.3f, target_diff=%.3f)", self._name, max_achievable_rise, delta_theoretical) + self.state.last_learning_status = "adjusted_theoretical_lte_0" + return False + + ratio = adjusted_theoretical / real_rise + + # Apply aggressiveness to the ratio to get more conservative Kint + # This ensures aggressiveness always has an effect, regardless of capacity saturation + ratio = ratio * self._aggressiveness + + current_coeff = self.state.coeff_indoor_cool if is_cool else self.state.coeff_indoor_heat + coeff_new = current_coeff * ratio + + # Validate coefficient - reject only truly invalid values (non-finite or <= 0) + if not math.isfinite(coeff_new) or coeff_new <= 0: + _LOGGER.warning("%s - Auto TPI: Invalid new indoor coeff: %.3f (non-finite or <= 0), skipping", self._name, coeff_new) + self.state.last_learning_status = "invalid_indoor_coeff" + return False + + # 4. Cap Coefficient + MAX_COEFF = self._max_coef_int + if coeff_new > MAX_COEFF: + _LOGGER.info("%s - Auto TPI: Calculated indoor coeff %.3f > %.1f, capping to %.1f before averaging", self._name, coeff_new, MAX_COEFF, MAX_COEFF) + coeff_new = MAX_COEFF + + old_coeff = self.state.coeff_indoor_cool if is_cool else self.state.coeff_indoor_heat + count = self.state.coeff_indoor_cool_autolearn if is_cool else self.state.coeff_indoor_autolearn + + # 5. Calculation Method + # 5. Calculation Method + # Cap the effective count to keep the system responsive + # Even if we have 1000 cycles history, we weigh the new sample as if we had at most 50 cycles. + effective_count = min(count, 50) + + if self._calculation_method == "average": + # Weighted average + # avg_coeff = ((old_coeff * count + coeff_new) / (count + 1)) + # We must use the current count (not incremented) as weight for old_coeff + + # If count is 0 (should not happen for valid state), treat as 1 + weight_old = max(effective_count, 1) + + avg_coeff = ((old_coeff * weight_old) + coeff_new) / (weight_old + 1) + _LOGGER.debug("%s - Auto TPI: Weighted Average: old=%.3f (weight=%d, real_count=%d), new=%.3f, result=%.3f", self._name, old_coeff, weight_old, count, coeff_new, avg_coeff) + + else: # EMA + # EMA Smoothing (20% weight by default) + # new_avg = (old_avg * (1 - alpha)) + (new_sample * alpha) + alpha = self._get_adaptive_alpha(effective_count) + avg_coeff = (old_coeff * (1.0 - alpha)) + (coeff_new * alpha) + _LOGGER.debug("%s - Auto TPI: EMA: old=%.3f, new=%.3f, alpha=%.3f (eff_count=%d, real_count=%d), result=%.3f", self._name, old_coeff, coeff_new, alpha, effective_count, count, avg_coeff) + + # Apply minimum Kint threshold to maintain temperature responsiveness + if avg_coeff < MIN_KINT: + _LOGGER.warning( + "%s - Auto TPI: Calculated Kint %.4f is below minimum %.4f, capping to minimum", + self._name, avg_coeff, MIN_KINT + ) + avg_coeff = MIN_KINT + + # Update counters + new_count = count + 1 + + if is_cool: + self.state.coeff_indoor_cool = avg_coeff + self.state.coeff_indoor_cool_autolearn = new_count + else: + self.state.coeff_indoor_heat = avg_coeff + self.state.coeff_indoor_autolearn = new_count + + _LOGGER.info( + "%s - Auto TPI: Learn indoor (%s). Old: %.3f, New calculated: %.3f (rise=%.3f), Averaged: %.3f (count: %d)", + self._name, + "cool" if is_cool else "heat", + old_coeff, + coeff_new, + real_rise, + avg_coeff, + new_count, + ) + + # Reset boost counter after successful learning + if hasattr(self.state, "consecutive_boosts"): + self.state.consecutive_boosts = 0 + + # Reset regime change flag after consuming the boost + if self._continuous_learning and self.state.regime_change_detected: + _LOGGER.debug("%s - Auto TPI: Regime change alpha consumed, resetting flag", self._name) + self.state.regime_change_detected = False + + return adjusted_theoretical - real_rise # Return the error: Expected Rise - Actual Rise + + def _learn_outdoor(self, current_temp_in: float, current_temp_out: float, is_cool: bool = False) -> bool: + """Learn outdoor coefficient.""" + gap_in = self.state.last_order - current_temp_in + gap_out = self.state.last_order - current_temp_out + + # Validation delta_out (moved here) + if abs(gap_out) < 0.05: + _LOGGER.debug("%s - Auto TPI: Cannot learn outdoor - gap_out too small (%.3f)", self._name, abs(gap_out)) + self.state.last_learning_status = "gap_out_too_small" + return False + + if gap_out == 0: + _LOGGER.debug("%s - Auto TPI: Cannot learn outdoor - gap_out is 0", self._name) + self.state.last_learning_status = "gap_out_is_zero" + return False + + # ============================================================================= + # INTELLIGENT VALIDATION : Overshoot + # ============================================================================= + # An overshoot indicates that the model OVERESTIMATED the necessary power. + # This is VALUABLE information to correct Kext. + # + # BUT: We must filter external anomalies (open door, sun, etc.) + # → Overshoot without significant power = anomaly, not model error + + # CASE 1: Check that the setpoint did not change during the cycle + consigne_changed = abs(self._current_target_temp - self.state.last_order) > 0.1 + + if consigne_changed: + _LOGGER.debug("%s - Auto TPI: Cannot learn outdoor - consigne changed during cycle (%.1f → %.1f)", self._name, self.state.last_order, self._current_target_temp) + self.state.last_learning_status = "consigne_changed" + return False + + # CASE 2: Overshoot without significant power = external anomaly + # If we have an overshoot but we barely heated/cooled (power < 20%), + # it is an external anomaly (open door, sun), not a model error + if is_cool: + # In cool mode: overcooled if gap_in > 0 (temp < target) + # Acceptable only if we really cooled (power > 1% instead of 20%) + # If power is > 1% and we are overcooling, it means Kext is too high and should be reduced. + if gap_in > 0 and self.state.last_power < 0.01: + _LOGGER.debug("%s - Auto TPI: Cannot learn outdoor - Anomalous overcooling (gap_in=%.2f, power=%.1f%%)", self._name, gap_in, self.state.last_power * 100) + self.state.last_learning_status = "anomalous_overcooling" + return False + + # Directional Protection + # If we are overcooling (below target) BUT the temperature is Rising (going back to target) + # AND power is low, then natural recovery is happening. Do not lower Kext. + # However, if power is still significant, the system is actively cooling and Kext might be too high. + if gap_in > 0 and current_temp_in > self.state.last_temp_in and self.state.last_power < NATURAL_RECOVERY_POWER_THRESHOLD: + _LOGGER.debug( + "%s - Auto TPI: Skipping outdoor learning during natural undershoot recovery (Temp rising %.2f -> %.2f, power=%.1f%%)", + self._name, self.state.last_temp_in, current_temp_in, self.state.last_power * 100 + ) + self.state.last_learning_status = "warming_up_naturally" + return True # Considered handled (skipped) + + else: + # In heat mode: overheated if gap_in < 0 (temp > target) + # Acceptable only if we really heated (power > 1% instead of 20%) + # If power is > 1% and we are overheating, it means Kext is too high and should be reduced. + if gap_in < 0 and self.state.last_power < 0.01: + _LOGGER.debug("%s - Auto TPI: Cannot learn outdoor - Anomalous overheating (gap_in=%.2f, power=%.1f%%)", self._name, gap_in, self.state.last_power * 100) + self.state.last_learning_status = "anomalous_overheating" + return False + + # Directional Protection + # If we are overheating (above target) BUT the temperature is Falling (going back to target) + # AND power is low, then natural recovery is happening. Do not lower Kext. + # However, if power is still significant, the system is actively heating and Kext is likely too high. + if gap_in < 0 and current_temp_in < self.state.last_temp_in and self.state.last_power < NATURAL_RECOVERY_POWER_THRESHOLD: + _LOGGER.debug( + "%s - Auto TPI: Skipping outdoor learning during natural overshoot recovery (Temp falling %.2f -> %.2f, power=%.1f%%)", + self._name, self.state.last_temp_in, current_temp_in, self.state.last_power * 100 + ) + self.state.last_learning_status = "cooling_down_naturally" + return True # Considered handled (skipped) + + # If we get here with an overshoot AND significant power: + # → It is a real model error, we MUST learn from it + # → The Kext correction will help correct the underestimated external influence + _LOGGER.debug("%s - Auto TPI: Overshoot validation passed (gap_in=%.2f, power=%.1f%%) - proceeding with learning", self._name, gap_in, self.state.last_power * 100) + + # ratio_influence = gap_in / gap_out + current_indoor = self.state.coeff_indoor_cool if is_cool else self.state.coeff_indoor_heat + current_outdoor = self.state.coeff_outdoor_cool if is_cool else self.state.coeff_outdoor_heat + + # Calculate corrective term based on indoor error (Missing power = Gap_In * Kint) + # Shift this missing power to Outdoor term (Equivalent Kext = Missing Power / Gap_Out) + # correction = (Gap_In / Gap_Out) * Kint + # Target = Current_Kext + Correction + + correction = current_indoor * (gap_in / gap_out) + target_outdoor = current_outdoor + correction + + # Use target_outdoor as the new sample + coeff_new = target_outdoor + + # Validate coefficient + if not math.isfinite(coeff_new) or coeff_new <= 0: + _LOGGER.warning("%s - Auto TPI: Invalid new outdoor coeff: %.3f (non-finite or <= 0), skipping", self._name, coeff_new) + self.state.last_learning_status = "invalid_outdoor_coeff" + return False + + # Cap at 1.2 (Slightly relaxed to allow logic to work in extreme cases, but bounded) + MAX_KEXT = 1.2 + if coeff_new > MAX_KEXT: + _LOGGER.info("%s - Auto TPI: Calculated outdoor coeff %.3f > %.1f, capping to %.1f before averaging", self._name, coeff_new, MAX_KEXT, MAX_KEXT) + coeff_new = MAX_KEXT + + count = self.state.coeff_outdoor_cool_autolearn if is_cool else self.state.coeff_outdoor_autolearn + old_coeff = current_outdoor + + # Apply EMA or average + effective_count = min(count, 50) + + if self._calculation_method == "average": + # Kext counter starts at 0, so first cycle should have weight 0 + weight_old = effective_count + avg_coeff = ((old_coeff * weight_old) + coeff_new) / (weight_old + 1) + _LOGGER.debug("%s - Auto TPI: Outdoor Weighted Average: old=%.3f (weight=%d, real_count=%d), new=%.3f, result=%.3f", self._name, old_coeff, weight_old, count, coeff_new, avg_coeff) + else: # EMA + alpha = self._get_adaptive_alpha(effective_count) + avg_coeff = (old_coeff * (1.0 - alpha)) + (coeff_new * alpha) + _LOGGER.debug("%s - Auto TPI: Outdoor EMA: old=%.3f, new=%.3f, alpha=%.3f (eff_count=%d, real_count=%d), result=%.3f", self._name, old_coeff, coeff_new, alpha, effective_count, count, avg_coeff) + + new_count = count + 1 + + # We only cap if continuous learning is OFF, and we want to stop learning + if not self._continuous_learning: + # The standard threshold is 50 + initial weight + INT_CYCLES_THRESHOLD = 50 + self._avg_initial_weight + + indoor_autolearn_count = self.state.coeff_indoor_cool_autolearn if is_cool else self.state.coeff_indoor_autolearn + is_indoor_finished = indoor_autolearn_count >= INT_CYCLES_THRESHOLD + + # Kext learning stops (capped at 50) ONLY if: + # (kext_cycles >= 50) AND (not keep_ext_learning OR kint_cycles >= 50 + initial_weight). + # This ensures Kext always learns a minimum of 50 cycles (Standard minimum). + + EXT_CYCLES_THRESHOLD = 50 + is_kext_standard_finished = count >= EXT_CYCLES_THRESHOLD + + # stop_learning_now is not used here, only for final persistence check + + # new_count is NOT capped anymore to reflect the real number of cycles + pass # No cap + + self._calculate_retroactive_capacity(avg_coeff, old_coeff, is_cool) + + if is_cool: + self.state.coeff_outdoor_cool = avg_coeff + self.state.coeff_outdoor_cool_autolearn = new_count + else: + self.state.coeff_outdoor_heat = avg_coeff + self.state.coeff_outdoor_autolearn = new_count + + _LOGGER.info( + "%s - Auto TPI: Learn outdoor (%s). Old: %.3f, Correction: %.3f, Target: %.3f, Averaged: %.3f (count: %d)", + self._name, + "cool" if is_cool else "heat", + old_coeff, + correction, + coeff_new, + avg_coeff, + new_count, + ) + return True + + def _calculate_retroactive_capacity(self, avg_coeff: float, old_coeff: float, is_cool: bool) -> None: + """Calculate and apply retroactive capacity adjustment based on Kext change.""" + # RETRO-ACTIVE CAPACITY ADJUSTMENT + kext_diff = avg_coeff - old_coeff + delta_t_losses = 0.0 + max_capacity_attr = "" + + if is_cool: + delta_t_losses = self._current_temp_out - self._current_temp_in + max_capacity_attr = "max_capacity_cool" + else: + delta_t_losses = self._current_temp_in - self._current_temp_out + max_capacity_attr = "max_capacity_heat" + + delta_t_losses = max(0.0, delta_t_losses) + + if abs(kext_diff) > 0.0001 and delta_t_losses > 0.0: + current_capacity = getattr(self.state, max_capacity_attr) + if current_capacity > 0: + # We need to reverse the adiabatic calculation to find the implicit current "rise_rate" + # Old_Capacity = Rise_Rate + (Old_Kext * dT) + # Rise_Rate = Old_Capacity - (Old_Kext * dT) + # + # New_Capacity = Old_Capacity + kext_diff * dT + + # Using the shared formula: + # 1. Reverse to get invariant rise_rate + implicit_rise_rate = current_capacity - (old_coeff * delta_t_losses) + + # 2. Recalculate with new coefficient + new_capacity = self._calculate_adiabatic_capacity(implicit_rise_rate, avg_coeff, delta_t_losses) + + new_capacity = max(0.01, new_capacity) + + setattr(self.state, max_capacity_attr, new_capacity) + + _LOGGER.info( + "%s - Auto TPI: Adjusted %s: %.3f -> %.3f due to Kext change (diff: %.4f, dT: %.1f)", + self._name, max_capacity_attr, current_capacity, new_capacity, kext_diff, delta_t_losses + ) + + def _calculate_adiabatic_capacity(self, observed_rise_rate: float, k_ext: float, delta_t: float) -> float: + """Calculate adiabatic capacity (decoupled from losses). + + Formula: Capacity_adiabatic = Rise_Rate + (Kext * DeltaT) + """ + return observed_rise_rate + (k_ext * delta_t) + + def _should_learn_capacity(self) -> bool: + """Check if capacity learning should occur this cycle.""" + if not self.learning_active and not self._continuous_kext: + _LOGGER.debug("%s - Not learning capacity: learning and continuous kext are disabled", self._name) + return False + + # Determine if we are in bootstrap + in_bootstrap = ( + self.state.max_capacity_heat == 0 or + self.state.capacity_heat_learn_count < 3 + ) + + # Baseline thresholds + power_threshold = 0.80 + # Dynamic rise threshold: + # normally 0.05°C to avoid noise. + # BUT if power is near saturation (>95%), we might be limited by capacity, so we accept almost any rise (0.01°C). + # This allows max_capacity to decrease if the system struggles to heat (high power, low rise). + rise_threshold = 0.01 if self.state.last_power > 0.95 else 0.05 + + min_gap = 1.0 if self.state.capacity_heat_learn_count < 3 else 0.3 + + # Timeout Strategy: Force default capacity if bootstrap fails too many times + if in_bootstrap: + failures = self.state.bootstrap_failure_count + + if failures > 5: + # Force exit bootstrap with conservative capacity + _LOGGER.warning( + "%s - Bootstrap timeout after %d failures. Forcing default capacity 0.3°C/h and exiting bootstrap.", + self._name, failures + ) + self.state.max_capacity_heat = 0.3 + # We interpret this forced exit as having "learned" enough to stabilize + # Setting count to 3 ensures we use alpha=0.15 (stabilized) for future updates + self.state.capacity_heat_learn_count = 3 + self.state.bootstrap_failure_count = 0 # Reset counter + + # Persist default capacity to config + if self._hass and self._hass.loop and not self._hass.loop.is_closed(): + self._hass.async_create_task( + self.async_update_learning_data(capacity=0.3, is_heat_mode=True) + ) + + return False # Cycle handled (we set default), skip calculation logic for this cycle + + # Check Condition 1: Power + if self.state.last_power < power_threshold: + _LOGGER.debug( + "%s - Not learning capacity: power too low (%.1f%% < %.0f%%)", + self._name, self.state.last_power * 100, power_threshold * 100 + ) + if in_bootstrap: + self.state.bootstrap_failure_count += 1 + return False + + # Condition 1b: Minimum efficiency (heater on-time ratio) + # When efficiency is low, temperature rise from external factors (sun, window close) + # gets amplified in capacity calculation, causing outlier spikes. + if self._last_cycle_power_efficiency < MIN_EFFICIENCY_FOR_CAPACITY: + _LOGGER.debug( + "%s - Not learning capacity: efficiency too low (%.1f%% < %.0f%%) - external factors may dominate", + self._name, self._last_cycle_power_efficiency * 100, MIN_EFFICIENCY_FOR_CAPACITY * 100 + ) + if in_bootstrap: + self.state.bootstrap_failure_count += 1 + return False + + # Condition 2: Significant rise + real_rise = self._current_temp_in - self.state.last_temp_in + if real_rise < rise_threshold: + _LOGGER.debug( + "%s - Not learning capacity: rise too small (%.3f < %.2f°C)", + self._name, real_rise, rise_threshold + ) + if in_bootstrap: + self.state.bootstrap_failure_count += 1 + return False + + # Condition 3: Adequate gap (stricter during bootstrap) + target_diff = self._current_target_temp - self.state.last_temp_in + if target_diff < min_gap: + _LOGGER.debug( + "%s - Not learning capacity: gap too small (%.2f < %.1f°C)", + self._name, target_diff, min_gap + ) + # Note: We don't necessarily increment failure count for "small gap" + # as this is not a "failed attempt" to heat, but rather "no need to heat much". + # But if we are in bootstrap, we WANT larger gaps. + # Let's be conservative and NOT increment here to avoid relaxing just because setpoint is close. + return False + + return True + + async def _learn_capacity(self, power: float, delta_t: float, rise: float, + efficiency: float, k_ext: float) -> bool: + """Learn heating capacity using simple EWMA with adiabatic correction. + + Inspired by regul2.py parameter estimation approach. + + Args: + power: Heating power ratio (0-1) + delta_t: Temperature gap (Tin - Tout) in °C + rise: Observed temperature rise in °C + efficiency: Cycle efficiency (0-1) + k_ext: Current external coefficient + + Returns: + True if capacity was updated and RELOAD triggered, False otherwise (or no reload needed) + """ + # Calculate observed capacity (with thermal losses included) + cycle_duration_h = self._cycle_min / 60.0 + # Check for division by zero + if cycle_duration_h * efficiency <= 0: + return False + + observed_rise_rate = rise / (cycle_duration_h * efficiency) + + # Adiabatic correction: add back the estimated losses + # This decouples heating capacity from thermal losses + adiabatic_capacity = self._calculate_adiabatic_capacity(observed_rise_rate, k_ext, delta_t) + + # Basic validation (physical bounds) + if adiabatic_capacity <= 0 or adiabatic_capacity > 20.0: + _LOGGER.debug( + "%s - Capacity measurement out of bounds: %.2f°C/h, skipping", + self._name, adiabatic_capacity + ) + return False + + # Capacity learning with adaptive weighting: + # - Bootstrap (<3 cycles): EMA with alpha=0.4 for fast convergence + # - Transition (3-MAX_WEIGHT cycles):EWMA alpha decreases as 1/(count+1) + # - Stable (>MAX_WEIGHT cycles): EMA with alpha=0.05 for outlier resistance + count = self.state.capacity_heat_learn_count + MAX_CAPACITY_WEIGHT = 20 # After 20 cycles, switch to pure EMA + STABLE_ALPHA = 0.05 # Fixed alpha for mature model + + old_capacity = self.state.max_capacity_heat + + if old_capacity == 0: + # First measurement: take directly + self.state.max_capacity_heat = adiabatic_capacity + effective_alpha = 1.0 + elif count < 3: + # Bootstrap: EWMA with high alpha for fast convergence + alpha = 0.4 + self.state.max_capacity_heat = (1 - alpha) * old_capacity + alpha * adiabatic_capacity + effective_alpha = alpha + elif count < MAX_CAPACITY_WEIGHT: + # Transition: weighted average (alpha equivalent = 1/(count+1)) + # New value gets weight=1, old value gets weight=count + self.state.max_capacity_heat = (old_capacity * count + adiabatic_capacity) / (count + 1) + effective_alpha = 1.0 / (count + 1) + else: + # Stable: pure EMA with fixed low alpha + self.state.max_capacity_heat = (1 - STABLE_ALPHA) * old_capacity + STABLE_ALPHA * adiabatic_capacity + effective_alpha = STABLE_ALPHA + + # Clamp protection: limit capacity change to ±50% per cycle (except during bootstrap) + MAX_CHANGE_RATIO = 1.5 + if old_capacity > 0 and count >= 3: + min_allowed = old_capacity / MAX_CHANGE_RATIO + max_allowed = old_capacity * MAX_CHANGE_RATIO + if self.state.max_capacity_heat < min_allowed or self.state.max_capacity_heat > max_allowed: + clamped_value = max(min_allowed, min(max_allowed, self.state.max_capacity_heat)) + _LOGGER.warning( + "%s - Capacity clamped: %.2f -> %.2f (allowed: %.2f - %.2f)", + self._name, self.state.max_capacity_heat, clamped_value, min_allowed, max_allowed + ) + self.state.max_capacity_heat = clamped_value + + self.state.capacity_heat_learn_count += 1 + + # Store in history for confidence calculation + if not hasattr(self, '_capacity_history'): + self._capacity_history = [] + self._capacity_history.append(self.state.max_capacity_heat) + if len(self._capacity_history) > 10: + self._capacity_history.pop(0) + + _LOGGER.info( + "%s - Capacity learned: %.2f°C/h (count: %d, alpha: %.3f)", + self._name, self.state.max_capacity_heat, + self.state.capacity_heat_learn_count, effective_alpha + ) + + # Reset failure count on success + self.state.bootstrap_failure_count = 0 + + return False + + def _get_capacity_confidence(self) -> float: + """Calculate capacity learning confidence based on CV (coefficient of variation). + + Similar to tau_reliability() in regul2.py. + + Returns: + Confidence score from 0.0 (no confidence) to 1.0 (high confidence) + """ + # Need minimum samples + if self.state.capacity_heat_learn_count < 3: + return 0.3 + + # Need history + if not hasattr(self, '_capacity_history'): + self._capacity_history = [] + + if len(self._capacity_history) < 3: + return 0.5 + + # Calculate coefficient of variation (CV) + mean_cap = statistics.mean(self._capacity_history) + if mean_cap <= 0: + return 0.0 + + std_cap = statistics.pstdev(self._capacity_history) + cv = std_cap / mean_cap + + # Confidence decreases with CV + # CV = 0.1 → confidence = 0.90 + # CV = 0.3 → confidence = 0.70 + # CV = 0.5 → confidence = 0.50 + # CV > 1.0 → confidence = 0.0 + confidence = max(0.0, min(1.0, 1.0 - cv)) + + return confidence + + def _check_deboost(self, is_heat: bool, real_rise: float, adjusted_theoretical: float) -> bool: + """Check if we should reduce indoor coefficient after excessive performance. + + Only activates after MIN_DEBOOST_CYCLES to let normal learning stabilize first. + Returns True if deboost was applied. + """ + MIN_DEBOOST_CYCLES = 20 + + if is_heat: + count = self.state.coeff_indoor_autolearn + current_kint = self.state.coeff_indoor_heat + else: + count = self.state.coeff_indoor_cool_autolearn + current_kint = self.state.coeff_indoor_cool + + # Wait for learning to stabilize before applying deboost + if count < MIN_DEBOOST_CYCLES: + return False + + # If we achieved more than expected, consider reducing coefficient + if real_rise <= adjusted_theoretical * 1.2: # Need 20% overshoot to trigger + return False + + DEBOOST_FACTOR = 0.95 + + # Calculate target Kint + target_kint = current_kint * DEBOOST_FACTOR + effective_count = min(count, 50) + old = current_kint + + # Apply same weighting logic as Kext overshoot correction + if self._calculation_method == "average": + boosted_weight = max(1, int(effective_count / OVERSHOOT_CORRECTION_BOOST)) + new_kint = ((old * boosted_weight) + target_kint) / (boosted_weight + 1) + _LOGGER.debug( + "%s - Deboost Kint (Average): old=%.4f, target=%.4f, weight=%d (boosted from %d), result=%.4f", + self._name, old, target_kint, boosted_weight, effective_count, new_kint + ) + else: # EMA + base_alpha = self._get_adaptive_alpha(effective_count) + boosted_alpha = min(base_alpha * OVERSHOOT_CORRECTION_BOOST, 0.3) + new_kint = (old * (1.0 - boosted_alpha)) + (target_kint * boosted_alpha) + _LOGGER.debug( + "%s - Deboost Kint (EMA): old=%.4f, target=%.4f, alpha=%.3f (boosted from %.3f), result=%.4f", + self._name, old, target_kint, boosted_alpha, base_alpha, new_kint + ) + + if is_heat: + if old > self._default_coef_int: + self.state.coeff_indoor_heat = max(new_kint, self._default_coef_int) + _LOGGER.info("%s - Deboosting Kint heat: %.3f → %.3f (weighted)", self._name, old, self.state.coeff_indoor_heat) + else: + if old > self._default_coef_int: + self.state.coeff_indoor_cool = max(new_kint, self._default_coef_int) + _LOGGER.info("%s - Deboosting Kint cool: %.3f → %.3f (weighted)", self._name, old, self.state.coeff_indoor_cool) + + # Reset boost counter + if hasattr(self.state, "consecutive_boosts"): + self.state.consecutive_boosts = 0 + + return True + + def _correct_kext_overshoot(self, overshoot: float, is_cool: bool) -> bool: + """Aggressively reduce Kext when room is overshooting with significant power. + + This method is called when the room temperature exceeds the setpoint + while heat is still being applied. This indicates Kext is too high. + + Args: + overshoot: Temperature above setpoint (positive value) in °C + is_cool: True if in cooling mode + + Returns: + True if correction was applied, False otherwise + """ + # Feature flag check + if not self.state.allow_kext_overshoot: + return False + + current_kext = self.state.coeff_outdoor_cool if is_cool else self.state.coeff_outdoor_heat + current_kint = self.state.coeff_indoor_cool if is_cool else self.state.coeff_indoor_heat + + # Calculate delta_ext for the correction + delta_ext = self.state.last_order - self._current_temp_out + if abs(delta_ext) < 0.1: + _LOGGER.debug("%s - Auto TPI: Cannot correct Kext overshoot - delta_ext too small (%.2f)", self._name, delta_ext) + return False + + # Calculate how much Kext should be reduced + # At setpoint, Power = Kext * delta_ext + # To allow temperature to fall, we need to reduce power by at least: overshoot * Kint + # So: needed_power_reduction = overshoot * Kint + # And: needed_kext_reduction = needed_power_reduction / delta_ext + needed_reduction = (overshoot * current_kint) / delta_ext + + # Target Kext that would produce correct power at setpoint + target_kext = max(0.001, current_kext - needed_reduction) + + # Get base alpha for calculation + count = self.state.coeff_outdoor_cool_autolearn if is_cool else self.state.coeff_outdoor_autolearn + effective_count = min(count, 50) + + old_kext = current_kext + + if self._calculation_method == "average": + # For average mode: reduce effective weight to give more influence to correction + # Instead of weight = effective_count, use weight / OVERSHOOT_CORRECTION_BOOST + boosted_weight = max(1, int(effective_count / OVERSHOOT_CORRECTION_BOOST)) + new_kext = ((old_kext * boosted_weight) + target_kext) / (boosted_weight + 1) + _LOGGER.debug( + "%s - Auto TPI: Overshoot correction (Average): old=%.4f, target=%.4f, weight=%d (boosted from %d), result=%.4f", + self._name, old_kext, target_kext, boosted_weight, effective_count, new_kext + ) + else: # EMA + # Use boosted alpha for faster correction + base_alpha = self._get_adaptive_alpha(effective_count) + boosted_alpha = min(base_alpha * OVERSHOOT_CORRECTION_BOOST, 0.3) + new_kext = (old_kext * (1.0 - boosted_alpha)) + (target_kext * boosted_alpha) + _LOGGER.debug( + "%s - Auto TPI: Overshoot correction (EMA): old=%.4f, target=%.4f, alpha=%.3f (boosted from %.3f), result=%.4f", + self._name, old_kext, target_kext, boosted_alpha, base_alpha, new_kext + ) + + # Ensure Kext doesn't go below minimum + new_kext = max(0.001, new_kext) + + if is_cool: + self.state.coeff_outdoor_cool = new_kext + else: + self.state.coeff_outdoor_heat = new_kext + + self.state.last_learning_status = "corrected_kext_overshoot" + self._learning_just_completed = True + + _LOGGER.info( + "%s - Auto TPI: Overshoot correction applied! Kext: %.4f -> %.4f (overshoot=%.2f°C, power=%.1f%%)", + self._name, old_kext, new_kext, overshoot, self.state.last_power * 100 + ) + + return True + + def _correct_kint_insufficient_rise(self, target_diff: float, temp_progress: float, is_cool: bool) -> bool: + """Boost Kint when temperature stagnates despite significant gap to setpoint. + + This method is called when: + - target_diff > INSUFFICIENT_RISE_GAP_THRESHOLD (0.3°C) + - temp_progress < 0.02 (temperature stagnating) + - power < 0.99 (not saturated) + + Instead of incorrectly adjusting Kext (which would happen in outdoor learning), + we boost Kint to increase power output. + + Args: + target_diff: The gap between setpoint and room temperature (positive value) + temp_progress: Temperature change during the cycle (can be negative) + is_cool: True if in cooling mode + + Returns: + True if correction was applied, False otherwise + """ + # Feature flag check + if not self.state.allow_kint_boost: + return False + + # Check if we've hit the max consecutive boosts limit + if self.state.consecutive_boosts >= MAX_CONSECUTIVE_KINT_BOOSTS: + _LOGGER.warning( + "%s - Auto TPI: Kint boost skipped - max consecutive boosts (%d) reached. Possible undersized heating.", + self._name, MAX_CONSECUTIVE_KINT_BOOSTS + ) + self.state.last_learning_status = "max_kint_boosts_reached" + # Send notification if enabled (only once per limit hit) + if self._enable_notification and self.state.consecutive_boosts == MAX_CONSECUTIVE_KINT_BOOSTS: + self._hass.async_create_task(self._notify_undersized_heating()) + return False + + current_kint = self.state.coeff_indoor_cool if is_cool else self.state.coeff_indoor_heat + + # Calculate proportional boost based on gap size + # Base boost is 8%, but increases slightly with larger gaps + # For gap = 0.3°C: boost = 8%, for gap = 0.6°C: boost ≈ 10% + gap_factor = min(target_diff / INSUFFICIENT_RISE_GAP_THRESHOLD, 2.0) # Cap at 2x + base_boost_percent = (INSUFFICIENT_RISE_BOOST_FACTOR - 1.0) * gap_factor + + target_kint = current_kint * (1.0 + base_boost_percent) + + count = self.state.coeff_indoor_cool_autolearn if is_cool else self.state.coeff_indoor_autolearn + effective_count = min(count, 50) + old_kint = current_kint + + # Apply same weighting logic as Kext overshoot correction + if self._calculation_method == "average": + boosted_weight = max(1, int(effective_count / OVERSHOOT_CORRECTION_BOOST)) + new_kint = ((old_kint * boosted_weight) + target_kint) / (boosted_weight + 1) + _LOGGER.debug( + "%s - Boost Kint (Average): old=%.4f, target=%.4f, weight=%d (boosted from %d), result=%.4f", + self._name, old_kint, target_kint, boosted_weight, effective_count, new_kint + ) + else: # EMA + base_alpha = self._get_adaptive_alpha(effective_count) + boosted_alpha = min(base_alpha * OVERSHOOT_CORRECTION_BOOST, 0.3) + new_kint = (old_kint * (1.0 - boosted_alpha)) + (target_kint * boosted_alpha) + _LOGGER.debug( + "%s - Boost Kint (EMA): old=%.4f, target=%.4f, alpha=%.3f (boosted from %.3f), result=%.4f", + self._name, old_kint, target_kint, boosted_alpha, base_alpha, new_kint + ) + + # Cap to max coefficient + new_kint = min(new_kint, self._max_coef_int) + + # Ensure minimum Kint + new_kint = max(new_kint, MIN_KINT) + + # Check if we actually changed anything (might hit cap) + if abs(new_kint - current_kint) < 0.001: + _LOGGER.debug( + "%s - Auto TPI: Kint correction skipped - already at limit (current=%.3f, max=%.3f)", + self._name, current_kint, self._max_coef_int + ) + return False + + old_kint = current_kint + + if is_cool: + self.state.coeff_indoor_cool = new_kint + else: + self.state.coeff_indoor_heat = new_kint + + self.state.last_learning_status = "corrected_kint_insufficient_rise" + self._learning_just_completed = True + + _LOGGER.info( + "%s - Auto TPI: Kint correction applied! Kint: %.4f -> %.4f (gap=%.2f°C, progress=%.2f°C, power=%.1f%%, boost #%d)", + self._name, old_kint, new_kint, target_diff, temp_progress, self.state.last_power * 100, self.state.consecutive_boosts + 1 + ) + + # Increment consecutive boosts counter + self.state.consecutive_boosts += 1 + + return True + + async def _notify_undersized_heating(self): + """Send notification when max consecutive Kint boosts is reached.""" + title = f"Versatile Thermostat: Auto TPI Warning for {self._name}" + message = ( + f"Auto TPI has reached the maximum consecutive Kint boost limit ({MAX_CONSECUTIVE_KINT_BOOSTS}). " + f"The temperature is not rising despite increased power demand. " + f"This may indicate undersized heating or abnormal heat loss. " + f"Learning will continue normally, but Kint boosting is paused until external temperature rises." + ) + + try: + await self._hass.services.async_call( + "persistent_notification", + "create", + { + "title": title, + "message": message, + "notification_id": f"autotpi_undersized_heating_{self._unique_id}", + }, + blocking=False, + ) + _LOGGER.warning("%s - Auto TPI: Undersized heating notification sent.", self._name) + except Exception as e: + _LOGGER.error("%s - Auto TPI: Error sending undersized heating notification: %s", self._name, e) + + async def _detect_failures(self, current_temp_in: float): + """Detect system failures.""" + OFFSET_FAILURE = 1.0 + MIN_LEARN_FOR_DETECTION = 25 + + failure_detected = False + reason = "unknown" + + if ( + self.state.last_state == "heat" + and self.state.last_power >= self.saturation_threshold + and current_temp_in < self.state.last_order - OFFSET_FAILURE + and current_temp_in < self.state.last_temp_in + and self.state.coeff_indoor_autolearn > MIN_LEARN_FOR_DETECTION + ): + failure_detected = True + reason = "Temperature dropped while heating at full power" + _LOGGER.warning("%s - Auto TPI: Failure detected in HEAT mode at saturation (power=%.1f%%)", self._name, self.state.last_power * 100) + + elif ( + self.state.last_state == "cool" + and self.state.last_power >= self.saturation_threshold + and current_temp_in > self.state.last_order + OFFSET_FAILURE + and current_temp_in > self.state.last_temp_in + and self.state.coeff_indoor_autolearn > MIN_LEARN_FOR_DETECTION + ): + failure_detected = True + reason = "Temperature rose while cooling at full power" + _LOGGER.warning("%s - Auto TPI: Failure detected in COOL mode at saturation (power=%.1f%%)", self._name, self.state.last_power * 100) + + if failure_detected: + self.state.consecutive_failures += 1 + if self.state.consecutive_failures >= 3: + if self._continuous_learning: + # In continuous learning mode, don't stop learning - just skip faulty cycles + _LOGGER.warning( + "%s - Auto TPI: %d consecutive failures detected in continuous mode. " + "Skipping faulty cycles and continuing learning. Reason: %s", + self._name, + self.state.consecutive_failures, + reason, + ) + # Reset the counter to allow future failure detection + self.state.consecutive_failures = 0 + else: + # Standard mode: disable learning after 3 consecutive failures + self.state.autolearn_enabled = False + _LOGGER.error( + "%s - Auto TPI: Learning disabled due to %d consecutive failures.", + self._name, + self.state.consecutive_failures, + ) + + # Send persistent notification + # Retrieve the message from translations + # We use the "exceptions" category in strings.json + # The key is "component.versatile_thermostat.exceptions.auto_tpi_learning_stopped.message" + title = "Versatile Thermostat: Auto TPI Learning Stopped" + try: + translations = await translation.async_get_translations( + self._hass, + self._hass.config.language, + "exceptions", + {DOMAIN} + ) + + # Key format for exceptions: component.{domain}.exceptions.{key}.message + key = f"component.{DOMAIN}.exceptions.auto_tpi_learning_stopped.message" + message_template = translations.get(key) + + if message_template: + message = message_template.format(name=self._name, reason=reason) + else: + # Fallback if translation not found + message = f"Auto TPI learning for {self._name} has been stopped due to 3 consecutive failures. Reason: {reason}. Please check your configuration." + + await self._hass.services.async_call( + "persistent_notification", + "create", + { + "title": title, + "message": message, + "notification_id": f"autotpi_learning_stopped_{self._unique_id}", + }, + blocking=False, + ) + except Exception as e: + _LOGGER.error("%s - Auto TPI: Error sending persistent notification: %s", self._name, e) + + else: + self.state.consecutive_failures = 0 + + @property + def saturation_threshold(self) -> float: + """The saturation power threshold (default 1.0, 100%).""" + # This property is expected to be overridden by the mixing/main component. + # Defaulting to 1.0 for self-contained use if not overridden. + return 1.0 + + def calculate_power(self, setpoint: float, temp_in: float, temp_out: float, state_str: str) -> float: + """Calculate TPI power, using aggressive coefficients during bootstrap.""" + + # Bootstrap logic: aggressive coefficients + in_bootstrap = ( + state_str == "heat" and + (self.state.max_capacity_heat == 0 or self.state.capacity_heat_learn_count < 3) + ) + + saved_kint = self.state.coeff_indoor_heat + saved_kext = self.state.coeff_outdoor_heat + + if in_bootstrap: + self.state.coeff_indoor_heat = 1.0 + self.state.coeff_outdoor_heat = 0.1 + + try: + return self._calculate_power_tpi(setpoint, temp_in, temp_out, state_str) + finally: + if in_bootstrap: + self.state.coeff_indoor_heat = saved_kint + self.state.coeff_outdoor_heat = saved_kext + + def _calculate_power_tpi(self, setpoint: float, temp_in: float, temp_out: float, state_str: str) -> float: + """Normal TPI proportional control.""" + if temp_out is None: + return 0.0 + + direction = 1 if state_str == "heat" else -1 + delta_in = setpoint - temp_in + delta_out = setpoint - temp_out + + if state_str == "cool": + coeff_int = self.state.coeff_indoor_cool + coeff_ext = self.state.coeff_outdoor_cool + else: + coeff_int = self.state.coeff_indoor_heat + coeff_ext = self.state.coeff_outdoor_heat + + offset = self.state.offset + power = (direction * delta_in * coeff_int) + (direction * delta_out * coeff_ext) + offset + return max(0.0, min(1.0, power)) + + @staticmethod + def _remove_outliers_iqr(values: list[float]) -> list[float]: + """ + Remove outliers using Interquartile Range (IQR) method. + Keeps values within [Q1 - 1.5*IQR, Q3 + 1.5*IQR]. + """ + if len(values) < 4: + return values + + sorted_values = sorted(values) + n = len(sorted_values) + + q1_idx = n // 4 + q3_idx = (3 * n) // 4 + + q1 = sorted_values[q1_idx] + q3 = sorted_values[q3_idx] + iqr = q3 - q1 + + lower_bound = q1 - 1.5 * iqr + upper_bound = q3 + 1.5 * iqr + + return [v for v in values if lower_bound <= v <= upper_bound] + + def _get_power_at_time_sample_hold(self, target_dt: datetime, sorted_power: list, start_idx: int = 0) -> tuple[Optional[float], int]: + """ + Get the power value at a specific time using sample-and-hold logic. + + For event-driven sensors, returns the last known power value before or at target_dt. + This is more appropriate for capacity calibration where power may stay stable + for long periods without new history entries. + + Args: + target_dt: The datetime to find power for + sorted_power: Power history sorted by time (ascending) + start_idx: Index to start searching from (optimization) + + Returns: + Tuple of (power value in percent, next index to continue from) + """ + if not sorted_power: + return None, 0 + + last_valid_power = None + last_valid_idx = start_idx + + # Find the last power entry that is <= target_dt + for i in range(start_idx, len(sorted_power)): + state = sorted_power[i] + try: + state_dt = state.last_changed + + if state_dt > target_dt: + # We've passed the target time, use the last valid power + break + + # This entry is at or before target_dt + state_value = getattr(state, "state", None) + if state_value not in ["unknown", "unavailable", None]: + try: + last_valid_power = float(state_value) + last_valid_idx = i + except (ValueError, TypeError): + pass + + except (AttributeError, TypeError): + continue + + return last_valid_power, last_valid_idx + + async def calculate_capacity_from_slope_sensor( + self, + slope_history: list, + power_history: list, + min_power_threshold: float = 0.95, + kext_coeff: float = 0.0, + current_indoor_temp: Optional[float] = None, + current_outdoor_temp: Optional[float] = None, + ) -> dict: + """ + Calculate ADIABATIC capacity using temperature_slope and power_percent sensor histories. + + ALGORITHM: + 1. Match slope points with power values at the same time + 2. Keep points where power >= threshold AND slope direction is correct + 3. Remove outliers using IQR method + 4. Use 75th percentile (biases toward higher/adiabatic values) + 5. Add Kext compensation: capacity = percentile_75 + Kext × avg_delta_T + + Args: + slope_history: History of temperature_slope sensor + power_history: History of power_percent sensor + min_power_threshold: Minimum power (0.0-1.0) to consider. Default 0.95 (95%) + kext_coeff: Current Kext coefficient for adiabatic correction + current_indoor_temp: Current indoor temperature for delta_T estimation + current_outdoor_temp: Current outdoor temperature for delta_T estimation + + Returns: + Dictionary with adiabatic capacity result and metrics + """ + # Always True now + is_heat_mode = True + power_threshold_percent = min_power_threshold * 100.0 + + _LOGGER.debug( + "%s - Capacity Calibration: Analyzing %d slope points and %d power points (threshold=%.0f%%)", + self._name, + len(slope_history) if slope_history else 0, + len(power_history) if power_history else 0, + power_threshold_percent, + ) + + if not slope_history: + return {"success": False, "error": "No temperature slope history found", "samples_used": 0} + + if not power_history: + return {"success": False, "error": "No power percent history found", "samples_used": 0} + + # Collect valid slope values + raw_slopes = [] + rejected_low_power = 0 + rejected_wrong_direction = 0 + rejected_invalid = 0 + + # Sort histories by time to enable O(N+M) matching + sorted_slopes = sorted(slope_history, key=lambda s: s.last_changed) + sorted_power = sorted(power_history, key=lambda s: s.last_changed) + + power_idx = 0 + for slope_state in sorted_slopes: + try: + slope_dt = slope_state.last_changed + slope_str = getattr(slope_state, "state", None) + + if slope_str in ["unknown", "unavailable", None]: + rejected_invalid += 1 + continue + + slope_value = float(slope_str) + + # Find power value using sample-and-hold logic (handles event-driven sensors) + power, power_idx = self._get_power_at_time_sample_hold(slope_dt, sorted_power, start_idx=power_idx) + + if power is None: + rejected_invalid += 1 + continue + + # Check power threshold + if power < power_threshold_percent: + rejected_low_power += 1 + continue + + # Check slope direction (always heating check now) + if slope_value <= 0: + rejected_wrong_direction += 1 + continue + + raw_slopes.append(slope_value) + + except (ValueError, TypeError, AttributeError) as e: + _LOGGER.debug("%s - Capacity Calibration: Invalid slope state: %s", self._name, e) + rejected_invalid += 1 + continue + + _LOGGER.info( + "%s - Capacity Calibration: Found %d valid samples (rejected: %d low-power, %d wrong-direction, %d invalid)", + self._name, + len(raw_slopes), + rejected_low_power, + rejected_wrong_direction, + rejected_invalid, + ) + + if len(raw_slopes) < 2: + return { + "success": False, + "error": f"Not enough valid samples ({len(raw_slopes)} found, minimum 2 required)", + "samples_used": len(raw_slopes), + "rejection_stats": {"low_power": rejected_low_power, "wrong_direction": rejected_wrong_direction, "invalid": rejected_invalid}, + } + + # Remove outliers + filtered_slopes = self._remove_outliers_iqr(raw_slopes) + outliers_removed = len(raw_slopes) - len(filtered_slopes) + + _LOGGER.debug("%s - Capacity Calibration: Removed %d outliers, %d samples remaining", self._name, outliers_removed, len(filtered_slopes)) + + if len(filtered_slopes) < 2: + return { + "success": False, + "error": f"Not enough samples after outlier removal ({len(filtered_slopes)} remaining)", + "samples_used": len(filtered_slopes), + "samples_before_filter": len(raw_slopes), + } + + # Calculate 75th percentile (biases toward adiabatic - higher values) + # Higher slopes = less heat loss = closer to adiabatic + sorted_slopes = sorted(filtered_slopes) + n = len(sorted_slopes) + + # 75th percentile index + p75_idx = int(0.75 * (n - 1)) + observed_capacity = sorted_slopes[p75_idx] + + # Estimate average delta_T for Kext compensation + # When power is at 100%, we typically have a significant delta_T + # Use current temperatures if available, otherwise use typical value + if current_indoor_temp is not None and current_outdoor_temp is not None: + avg_delta_t = abs(current_indoor_temp - current_outdoor_temp) + else: + # Typical delta_T when heating at high power (rough estimate: 10-15°C) + avg_delta_t = 12.0 # Conservative estimate + + # Apply Kext compensation for adiabatic capacity + # Capacity_adiabatic = Slope_observed + Kext × delta_T + kext_compensation = kext_coeff * avg_delta_t + capacity = observed_capacity + kext_compensation + + _LOGGER.debug( + "%s - Capacity Calibration: 75th percentile=%.3f, Kext=%.4f, delta_T=%.1f, compensation=%.3f, final=%.3f", + self._name, + observed_capacity, + kext_coeff, + avg_delta_t, + kext_compensation, + capacity, + ) + + # Ensure capacity is positive + if capacity <= 0.0: + _LOGGER.warning("%s - Capacity Calibration: Calculated capacity (%.3f) is not positive. Setting to 0.01.", self._name, capacity) + capacity = 0.01 + + # Calculate reliability based on sample count and variance + mean_slope = sum(filtered_slopes) / len(filtered_slopes) + variance = sum((s - mean_slope) ** 2 for s in filtered_slopes) / len(filtered_slopes) + std_dev = math.sqrt(variance) if variance > 0 else 0.0 + cv = std_dev / mean_slope if mean_slope > 0 else 0.0 # Coefficient of variation + + # Reliability: higher with more samples and lower variance + sample_factor = min(1.0, len(filtered_slopes) / 20.0) # Max at 20 samples + variance_factor = max(0.0, 1.0 - (cv / 2.0)) # Lower if high variance + reliability = 100.0 * sample_factor * variance_factor + + # Period calculation (in days) + period_days = 0.0 + if slope_history: + try: + timestamps = [s.last_changed for s in slope_history] + if timestamps: + start_date = min(timestamps) + end_date = max(timestamps) + period_days = (end_date - start_date).total_seconds() / 86400.0 + except (AttributeError, TypeError): + pass + + _LOGGER.info( + "%s - Capacity Calibration: Adiabatic Capacity=%.3f °C/h (observed=%.3f + Kext×ΔT=%.3f), Reliability=%.1f%%, Samples=%d", + self._name, + capacity, + observed_capacity, + kext_compensation, + reliability, + len(filtered_slopes), + ) + + return { + "success": True, + "capacity": round(capacity, 3), + "observed_capacity": round(observed_capacity, 3), + "kext_compensation": round(kext_compensation, 3), + "avg_delta_t": round(avg_delta_t, 1), + "samples_used": len(filtered_slopes), + "samples_before_filter": len(raw_slopes), + "outliers_removed": outliers_removed, + "reliability": round(reliability, 1), + "min_power_threshold": min_power_threshold, + "period": round(period_days, 1), + } + + async def service_calibrate_capacity( + self, + thermostat_entity_id: str, + ext_temp_entity_id: str, + save_to_config: bool, + min_power_threshold: float, + start_date: datetime | str | None = None, + end_date: datetime | str | None = None, + ) -> dict: + """ + Orchestrates the capacity calibration service using temperature_slope + and power_percent sensor histories. + + NEW ALGORITHM: + 1. Derives slope and power sensor entity IDs from thermostat entity ID + 2. Fetches history for both sensors + 3. Matches points where power >= threshold and slope direction is correct + 4. Removes outliers and calculates median as Capacity + + Args: + thermostat_entity_id: The climate entity ID (e.g., "climate.thermostat_salon") + ext_temp_entity_id: External temperature sensor (unused in new algorithm but kept for API compatibility) + save_to_config: Whether to save the result to config + start_date: Start of history period (default: 30 days ago) + end_date: End of history period (default: now) + min_power_threshold: Minimum power percentage (0.0-1.0) to consider a sample. + Default is 1.0 (100%). Lower values (e.g., 0.90) include more samples. + """ + # 1. Derive sensor entity IDs from thermostat entity ID + # climate.thermostat_salon -> sensor.thermostat_salon_temperature_slope + # climate.thermostat_salon -> sensor.thermostat_salon_power_percent + if thermostat_entity_id.startswith("climate."): + base_name = thermostat_entity_id.replace("climate.", "") + else: + base_name = thermostat_entity_id.split(".")[-1] + + slope_sensor_id = f"sensor.{base_name}_temperature_slope" + power_sensor_id = f"sensor.{base_name}_power_percent" + + _LOGGER.info("%s - Capacity calibration: Using slope sensor '%s' and power sensor '%s'", self._name, slope_sensor_id, power_sensor_id) + + # 2. Convert start_date and end_date to datetime objects + if isinstance(start_date, str): + _date = dt_util.parse_date(start_date) + start_date = dt_util.start_of_local_day(_date) if _date else None + + if isinstance(end_date, str): + _date = dt_util.parse_date(end_date) + _end_day_start = dt_util.start_of_local_day(_date) if _date else None + end_date = _end_day_start + timedelta(days=1) if _end_day_start else None + + # 3. Determine History Time Range + now = dt_util.now() + start_time = dt_util.as_utc(start_date) if start_date is not None else now - timedelta(days=30) + end_time = dt_util.as_utc(end_date) if end_date is not None else now + + _LOGGER.info("%s - Calibrating capacity using history from %s to %s", self._name, start_time, end_time) + + # Handle percentage value for min_power_threshold (e.g. 95 -> 0.95) + if min_power_threshold > 1.0: + _LOGGER.debug("%s - Converting min_power_threshold from %.1f to %.2f", self._name, min_power_threshold, min_power_threshold / 100.0) + min_power_threshold = min_power_threshold / 100.0 + + # 4. Fetch sensor histories in chunks to avoid timeouts and cope with gaps + entity_ids = [slope_sensor_id, power_sensor_id] + slope_history = [] + power_history = [] + + # We use 2-day chunks for robustness + chunk_delta = timedelta(days=2) + current_start = start_time + + while current_start < end_time: + current_end = min(current_start + chunk_delta, end_time) + _LOGGER.debug("%s - Fetching history chunk from %s to %s", self._name, current_start, current_end) + + try: + chunk_states = await get_instance(self._hass).async_add_executor_job( + partial( + history.get_significant_states, + self._hass, + current_start, + end_time=current_end, + entity_ids=entity_ids, + significant_changes_only=False, + ) + ) + + if chunk_states: + slope_history.extend(chunk_states.get(slope_sensor_id, [])) + power_history.extend(chunk_states.get(power_sensor_id, [])) + + except Exception as e: + _LOGGER.warning("%s - Error fetching history chunk %s to %s: %s", self._name, current_start, current_end, e) + + current_start = current_end + + _LOGGER.debug("%s - Fetched %d slope sensor states and %d power sensor states for capacity calibration.", self._name, len(slope_history), len(power_history)) + + # Check if sensors exist + if not slope_history: + _LOGGER.warning("%s - No history found for slope sensor '%s'. " "Make sure the sensor exists and has history enabled in recorder.", self._name, slope_sensor_id) + if not power_history: + _LOGGER.warning("%s - No history found for power sensor '%s'. " "Make sure the sensor exists and has history enabled in recorder.", self._name, power_sensor_id) + + # 5. Get Kext from HA config (not learned value) for adiabatic correction + kext_coeff = self._default_coef_ext + + # Get current temperatures from thermostat for delta_T estimation + current_indoor_temp = None + current_outdoor_temp = None + + # Try to get current thermostat state for indoor temp + thermostat_state = self._hass.states.get(thermostat_entity_id) + if thermostat_state: + try: + current_indoor_temp = float(thermostat_state.attributes.get("current_temperature", 0)) + except (ValueError, TypeError): + pass + + # Try to get outdoor temp from the external sensor + if ext_temp_entity_id: + outdoor_state = self._hass.states.get(ext_temp_entity_id) + if outdoor_state and outdoor_state.state not in ["unknown", "unavailable"]: + try: + current_outdoor_temp = float(outdoor_state.state) + except (ValueError, TypeError): + pass + + _LOGGER.debug( + "%s - Adiabatic correction params: Kext_config=%.4f, T_indoor=%.1f, T_outdoor=%.1f", + self._name, + kext_coeff, + current_indoor_temp if current_indoor_temp else 0, + current_outdoor_temp if current_outdoor_temp else 0, + ) + + # 6. Call calculation method with adiabatic correction + result = await self.calculate_capacity_from_slope_sensor( + slope_history, + power_history, + min_power_threshold=min_power_threshold, + kext_coeff=kext_coeff, + current_indoor_temp=current_indoor_temp, + current_outdoor_temp=current_outdoor_temp, + ) + + _LOGGER.info("%s - Capacity calibration result: %s", self._name, result) + + # 6. Save to config if requested + if result and isinstance(result, dict) and result.get("success"): + + max_capacity = result.get("capacity") + if max_capacity is not None: + # Rename capacity to max_capacity in result + if "capacity" in result: + del result["capacity"] + + result["max_capacity"] = max_capacity + + if save_to_config: + # Always heat mode + is_heat_mode = True + + await self.async_update_learning_data(capacity=max_capacity, is_heat_mode=is_heat_mode) + + _LOGGER.info( + "%s - Heating capacity calibrated and saved: %.3f °C/h", + self._name, max_capacity + ) + + return result + + async def _try_pre_bootstrap_calibration(self) -> float | None: + """ + Try to calibrate capacity from historical data before starting bootstrap. + + Calls the calibration service internally with min_power_threshold=80%. + If reliability >= MIN_PRE_BOOTSTRAP_CALIBRATION_RELIABILITY, returns max_capacity. + Otherwise, returns None to trigger bootstrap. + """ + try: + # Use the stored entity_id if available, otherwise fall back to unique_id + if self._entity_id: + thermostat_entity_id = self._entity_id + else: + thermostat_entity_id = f"climate.{self._unique_id}" + _LOGGER.warning( + "%s - Auto TPI: entity_id not set, falling back to unique_id-based entity_id: %s", + self._name, thermostat_entity_id + ) + + # Get external temperature entity from thermostat state if available + ext_temp_entity_id = "" + thermostat_state = self._hass.states.get(thermostat_entity_id) + if thermostat_state: + ext_temp_entity_id = thermostat_state.attributes.get("ext_current_temperature_entity_id", "") + + _LOGGER.debug( + "%s - Auto TPI: Attempting pre-bootstrap calibration with min_power_threshold=80%%", + self._name + ) + + result = await self.service_calibrate_capacity( + thermostat_entity_id=thermostat_entity_id, + ext_temp_entity_id=ext_temp_entity_id, + save_to_config=False, # Do not save yet, just check + min_power_threshold=0.80, # 80% power threshold for more samples + ) + + if not result or not result.get("success"): + error = result.get("error", "unknown error") if result else "no result" + _LOGGER.debug( + "%s - Auto TPI: Pre-bootstrap calibration failed: %s", + self._name, error + ) + return None + + reliability = result.get("reliability", 0.0) + max_capacity = result.get("max_capacity", 0.0) + + if reliability >= MIN_PRE_BOOTSTRAP_CALIBRATION_RELIABILITY and max_capacity > 0: + _LOGGER.info( + "%s - Auto TPI: Pre-bootstrap calibration returned reliability=%.1f%% (>= %.1f%%), capacity=%.2f °C/h", + self._name, reliability, MIN_PRE_BOOTSTRAP_CALIBRATION_RELIABILITY, max_capacity + ) + return max_capacity + else: + _LOGGER.debug( + "%s - Auto TPI: Pre-bootstrap calibration reliability too low (%.1f%% < %.1f%%) or capacity invalid (%.2f)", + self._name, reliability, MIN_PRE_BOOTSTRAP_CALIBRATION_RELIABILITY, max_capacity + ) + return None + + except Exception as e: + _LOGGER.warning( + "%s - Auto TPI: Pre-bootstrap calibration error: %s", + self._name, e + ) + return None + + @callback + def _capture_end_of_on_temp(self, _): + """Capture the temperature at the end of the ON pulse.""" + self.state.last_on_temp_in = self._current_temp_in + _LOGGER.debug("%s - Auto TPI: Captured end of ON temp: %.1f", self._name, self.state.last_on_temp_in) + self._timer_capture_remove_callback = None + + def update_realized_power(self, realized_percent: float): + """Update the power actually applied to the underlyings. + + Called by the handler when the realized power differs from the + requested on_percent. Sources of difference: + - timing constraints (min_activation_delay, min_deactivation_delay) + - max_on_percent clamping + - safety mode override + + This ensures learning uses the actual applied power, not the requested one. + """ + if self.state.cycle_active: + old = self.state.last_power + self.state.last_power = realized_percent + if abs(old - realized_percent) > 0.001: + _LOGGER.debug( + "%s - Auto TPI: Realized power updated: %.1f%% -> %.1f%%", + self._name, old * 100, realized_percent * 100 + ) + + async def on_cycle_started(self, on_time_sec: float, off_time_sec: float, on_percent: float, hvac_mode: str): + """Called when a TPI cycle starts.""" + # Detect if previous cycle was interrupted + is_expected_interruption = self._learning_just_completed + self._learning_just_completed = False # Reset the flag after check + + if self.state.cycle_active and not is_expected_interruption: + _LOGGER.info("%s - Auto TPI: Previous cycle was interrupted (not completed). Discarding it.", self._name) + # You could add specific logic here if needed (stats, etc) + + # Cancel any pending capture timer + if self._timer_capture_remove_callback: + self._timer_capture_remove_callback() + self._timer_capture_remove_callback = None + + self.state.cycle_active = True + + _LOGGER.debug("%s - Auto TPI: Cycle started. On: %.0fs, Off: %.0fs (%.1f%%), Mode: %s", self._name, on_time_sec, off_time_sec, on_percent * 100, hvac_mode) + + now = dt_util.now() + + # Snapshot current state for learning at the end of the cycle + self.state.last_temp_in = self._current_temp_in + self.state.last_temp_out = self._current_temp_out + self.state.last_order = self._current_target_temp + self.state.last_power = on_percent if on_percent is not None else 0.0 + self.state.last_on_temp_in = 0.0 # Reset + + # Save previous state before updating last_state (for first cycle detection) + self.state.previous_state = self.state.last_state + + # Map VThermHvacMode/HVACMode to internal state string + # hvac_mode is expected to be VThermHvacMode or string representation + mode_str = str(hvac_mode) + if mode_str == "heat" or mode_str == "heating": + self.state.last_state = "heat" + elif mode_str == "cool" or mode_str == "cooling": + self.state.last_state = "cool" + else: + self.state.last_state = "stop" + + self.state.cycle_start_date = now + self.state.last_update_date = now + + # Store current cycle params so on_cycle_completed() can read them without params + self.state.current_cycle_params = { + "on_time_sec": on_time_sec, + "off_time_sec": off_time_sec, + "on_percent": on_percent, + "hvac_mode": str(hvac_mode), + } + + # Schedule capture of temperature at the end of the ON pulse + if on_time_sec > 0: + self._timer_capture_remove_callback = async_call_later(self._hass, on_time_sec, self._capture_end_of_on_temp) + + # Calculate cold factor for this cycle + self.state.current_cycle_cold_factor = 0.0 + if self._heater_cooling_time > 0 and self.state.last_heater_stop_time: + # Ensure both datetimes are timezone-aware (handles legacy data) + last_stop = self.state.last_heater_stop_time + if last_stop.tzinfo is None: + last_stop = dt_util.as_local(last_stop) + elapsed_off = (now - last_stop).total_seconds() / 60.0 + if elapsed_off >= 0: + self.state.current_cycle_cold_factor = min(1.0, max(0.0, elapsed_off / self._heater_cooling_time)) + _LOGGER.debug( + "%s - Auto TPI: Cold factor calc: elapsed_off=%.1f min, cooling_time=%.1f min, factor=%.2f", + self._name, + elapsed_off, + self._heater_cooling_time, + self.state.current_cycle_cold_factor, + ) + + await self.async_save_data() + + def _should_learn_continuous_kext(self) -> bool: + """Check if we should proceed with continuous Kext learning.""" + if not self._continuous_kext: + return False + + # Must be bootstrapped (at least 1 outdoor sample) + # We check both modes as we don't know the future mode yet, + # but technically we should check the count for the CURRENT mode in _learn_kext_continuous. + # Here we just check if it's generally possible (any learning done). + # However, to be strict, we can defer the check to _learn_kext_continuous. + if self.state.coeff_outdoor_autolearn == 0 and self.state.coeff_outdoor_cool_autolearn == 0: + return False + + # Standard exclusions adapted from _should_learn + saturation_threshold = self.saturation_threshold + if not (0 < self.state.last_power < saturation_threshold): + return False + + if self._current_cycle_interrupted: + return False + + if self._central_boiler_off: + return False + + if self._current_is_heating_failure: + return False + + if self.state.consecutive_failures >= 3: + return False + + if self.state.previous_state == "stop": + return False + + if self.state.last_order == 0: + return False + + # Significant outdoor delta (> 1.0) + delta_out = self.state.last_order - self._current_temp_out + if abs(delta_out) < 1.0: + return False + + return True + + async def _learn_kext_continuous(self, current_temp_in: float, current_temp_out: float): + """Perform continuous Kext learning.""" + if not self._should_learn_continuous_kext(): + return + + is_heat = self.state.last_state == "heat" + is_cool = self.state.last_state == "cool" + + if not (is_heat or is_cool): + return + + # Check bootstrap for specific mode + count = self.state.coeff_outdoor_autolearn if is_heat else self.state.coeff_outdoor_cool_autolearn + if count == 0: + _LOGGER.debug("%s - Continuous Kext: Not bootstrapped for %s mode", self._name, "heat" if is_heat else "cool") + self.state.last_learning_status = "continuous_kext_not_bootstrapped" + return + + # Check setpoint change + if abs(self._current_target_temp - self.state.last_order) > 0.1: + _LOGGER.debug("%s - Continuous Kext: Setpoint changed", self._name) + self.state.last_learning_status = "continuous_kext_setpoint_changed" + return + + target_temp = self.state.last_order + gap_in = target_temp - current_temp_in + gap_out = target_temp - current_temp_out + + # Avoid division by zero or small deltas + if abs(gap_out) < 1.0: + # Already checked in _should_learn but good to be safe + return + + current_indoor = self.state.coeff_indoor_heat if is_heat else self.state.coeff_indoor_cool + current_outdoor = self.state.coeff_outdoor_heat if is_heat else self.state.coeff_outdoor_cool + + # Formula: correction = Kint * (Gap_In / Gap_Out) + correction = current_indoor * (gap_in / gap_out) + target_outdoor = current_outdoor + correction + + # Validations + if not math.isfinite(target_outdoor) or target_outdoor <= 0: + _LOGGER.warning("%s - Continuous Kext: Invalid target Kext %.4f", self._name, target_outdoor) + self.state.last_learning_status = "continuous_kext_invalid" + return + + MAX_KEXT = 1.2 + if target_outdoor > MAX_KEXT: + target_outdoor = MAX_KEXT + + # EMA + alpha = self._continuous_kext_alpha + new_kext = (current_outdoor * (1.0 - alpha)) + (target_outdoor * alpha) + + # Update state in memory + if is_heat: + self.state.coeff_outdoor_heat = new_kext + else: + self.state.coeff_outdoor_cool = new_kext + + self.state.last_learning_status = f"continuous_kext_learned_{'cool' if is_cool else 'heat'}" + self._learning_just_completed = True + + _LOGGER.info( + "%s - Continuous Kext Learning (%s): Old=%.4f, Target=%.4f, New=%.4f (Alpha=%.3f, GapIn=%.2f, GapOut=%.2f)", + self._name, "heat" if is_heat else "cool", current_outdoor, target_outdoor, new_kext, alpha, gap_in, gap_out + ) + + # Persist immediately to config entry if Kext changed significantly (threshold: 0.001). + # This avoids having to rely on a startup sync, and keeps the config entry up to date. + if abs(new_kext - current_outdoor) > 0.001: + await self.async_update_learning_data( + coef_ext=new_kext, + is_heat_mode=is_heat + ) + + async def on_cycle_completed(self, e_eff: float = None, **_kw) -> None: + """Called when a TPI cycle completes.""" + # Validation logic (moved from old _tick) + now = dt_util.now() + + prev_params = self.state.current_cycle_params or {} + if self.state.cycle_start_date is not None and self.state.current_cycle_params is not None: + # Ensure cycle_start_date is timezone-aware + cycle_start = self.state.cycle_start_date + if cycle_start.tzinfo is None: + cycle_start = dt_util.as_local(cycle_start) + + elapsed_minutes = (now - cycle_start).total_seconds() / 60 + expected_duration = self._cycle_min + tolerance = max(expected_duration * 0.1, 1.0) + + duration_diff = elapsed_minutes - expected_duration + + # Case 1: Cycle too short (likely forced restart due to preset/temp change or restart) + if duration_diff < -tolerance: + _LOGGER.debug( + "%s - Cycle too short: duration=%.1fmin (expected=%.1fmin). Likely forced restart. Skipping learning.", + self._name, + elapsed_minutes, + expected_duration, + ) + self.state.last_learning_status = "cycle_too_short" + # We return here because a short cycle shouldn't count towards total_cycles or update stop time + # (it was interrupted actively) + self.state.cycle_active = False + return + + # Case 2: Cycle too long (Gap/Silence detected) + if duration_diff > tolerance: + _LOGGER.debug( + "%s - Cycle gap detected: duration=%.1fmin (expected=%.1fmin, tolerance=%.1fmin). Resetting cycle but skipping learning.", + self._name, + elapsed_minutes, + expected_duration, + tolerance, + ) + # We do NOT return here. We allow update of total_cycles and last_heater_stop_time + self.state.last_learning_status = "cycle_gap_detected" + else: + # No start date or params, nothing to do + return + + on_time_sec = prev_params.get("on_time_sec", 0) + off_time_sec = prev_params.get("off_time_sec", 0) + + if not self.state.cycle_active: + _LOGGER.debug("%s - Auto TPI: Cycle completed but no cycle active. Ignoring.", self._name) + return + + self.state.cycle_active = False + + if e_eff is not None: + self.state.last_power = e_eff + + elapsed_minutes = (on_time_sec + off_time_sec) / 60 + on_time_minutes = on_time_sec / 60.0 + self.state.total_cycles += 1 + + # Update last_heater_stop_time if we were heating + if self.state.last_state == "heat": + self.state.last_heater_stop_time = dt_util.now() + + # Calculate Power Efficiency based on Heater Warm-up Time and Cold Factor + # heater_heating_time is the time for the heater to warm up when fully cold. + # effective_warm_up_time is the actual warm-up time in this cycle, adjusted by the cold_factor. + # This part of the ON time is considered 'ineffective' for room temperature rise. + self._last_cycle_power_efficiency = 1.0 + # effective_warm_up_time is the portion of the ON time used to heat up the radiator itself + effective_warm_up_time = self._heater_heating_time * self.state.current_cycle_cold_factor + + if effective_warm_up_time > 0 and on_time_minutes > 0: + # effective_time is the time remaining after the radiator is warmed up + effective_time = max(0.0, on_time_minutes - effective_warm_up_time) + self._last_cycle_power_efficiency = effective_time / on_time_minutes + + _LOGGER.debug( + "%s - Auto TPI: Power Efficiency calc: on_time=%.1f min, warm_up_time=%.1f, cold_factor=%.2f, eff_warm_up_time=%.1f, eff=%.2f", + self._name, + on_time_minutes, + self._heater_heating_time, + self.state.current_cycle_cold_factor, + effective_warm_up_time, + self._last_cycle_power_efficiency, + ) + + if self.learning_active: + _LOGGER.info( + "%s - Auto TPI: Cycle #%d completed after %.1f minutes (efficiency: %.2f)", self._name, self.state.total_cycles, elapsed_minutes, self._last_cycle_power_efficiency + ) + else: + _LOGGER.debug( + "%s - Auto TPI: Cycle #%d completed after %.1f minutes (efficiency: %.2f)", self._name, self.state.total_cycles, elapsed_minutes, self._last_cycle_power_efficiency + ) + + # Attempt learning + # Determine if in bootstrap + in_bootstrap = ( + self._current_hvac_mode == "heat" and + (self.state.max_capacity_heat == 0 or self.state.capacity_heat_learn_count < 3) + ) + # Check if cycle is significant enough for learning + # Significant if we had some effective heating time (efficiency > 0) + is_significant_cycle = self._last_cycle_power_efficiency > 0.0 + + # PHASE 1: Capacity Learning (independent of saturation check) + # Capacity learning needs high power cycles (>=80%), which may be saturated (100%) + # This must run independently of _should_learn() which rejects saturated power + real_rise = self._current_temp_in - self.state.last_temp_in + efficiency = self._last_cycle_power_efficiency + + # Check if cycle was flagged as invalid (e.g. gap detected) + if self.state.last_learning_status == "cycle_gap_detected": + _LOGGER.debug("%s - Auto TPI: Skipping capacity learning due to invalid cycle (gap detected)", self._name) + elif self._should_learn_capacity(): + await self._learn_capacity( + power=self.state.last_power, + delta_t=self._current_temp_in - self._current_temp_out, + rise=real_rise, + efficiency=efficiency, + k_ext=self.state.coeff_outdoor_heat + ) + if in_bootstrap: + _LOGGER.info( + "%s - Bootstrap cycle %d/%d completed, capacity: %.2f°C/h", + self._name, + self.state.capacity_heat_learn_count, + 3, # Total bootstrap cycles + self.state.max_capacity_heat + ) + + # PHASE 2: Kint/Kext Learning (requires non-saturated power) + # Skip during bootstrap (learn only capacity first) + if in_bootstrap: + _LOGGER.debug("%s - Auto TPI: In bootstrap mode, skipping Kint/Kext learning", self._name) + elif self._should_learn() and is_significant_cycle: + _LOGGER.info("%s - Auto TPI: Attempting to learn Kint/Kext from cycle data", self._name) + await self._perform_learning(self._current_temp_in, self._current_temp_out) + elif self._continuous_kext and is_significant_cycle and self._should_learn_continuous_kext(): + _LOGGER.info("%s - Continuous Kext: Learning active...", self._name) + await self._learn_kext_continuous(self._current_temp_in, self._current_temp_out) + else: + reason = self._get_no_learn_reason() + if not is_significant_cycle and reason == "unknown": + reason = "on_time_too_short_vs_heating_time" + + _LOGGER.debug("%s - Auto TPI: Not learning Kint/Kext this cycle: %s", self._name, reason) + # Only update status if it wasn't already set to "cycle_gap_detected" or other critical error + if self.state.last_learning_status != "cycle_gap_detected": + self.state.last_learning_status = reason + + # Check for failures + await self._detect_failures(self._current_temp_in) + + # Centralized persistence: Check if learning is finished and persist if needed + if self.learning_active: + await self.process_learning_completion() + + await self.async_save_data() + + + + def get_calculated_params(self) -> dict: + return self._calculated_params + + @property + def is_in_bootstrap(self) -> bool: + """Return True if the algorithm is in bootstrap mode (learning capacity).""" + return ( + self.state.max_capacity_heat == 0 or + self.state.capacity_heat_learn_count < 3 + ) + + @property + def learning_active(self) -> bool: + return self.state.autolearn_enabled + + @property + def int_cycles(self) -> int: + """Number of ACTUAL learning cycles completed for internal coefficient""" + is_cool_mode = self._current_hvac_mode == "cool" + if is_cool_mode: + return max(0, self.state.coeff_indoor_cool_autolearn - self._avg_initial_weight) + return max(0, self.state.coeff_indoor_autolearn - self._avg_initial_weight) + + @property + def ext_cycles(self) -> int: + """Number of learning cycles completed for external coefficient""" + is_cool_mode = self._current_hvac_mode == "cool" + if is_cool_mode: + return self.state.coeff_outdoor_cool_autolearn + return self.state.coeff_outdoor_autolearn + + @property + def heating_cycles_count(self) -> int: + """Number of total TPI cycles""" + return self.state.total_cycles + + @property + def time_constant(self) -> float: + """Thermal time constant in hours""" + if self.state.coeff_indoor_heat > 0: + return round(1.0 / self.state.coeff_indoor_heat, 2) + return 0.0 + + @property + def confidence(self) -> float: + """Confidence level in the learned model (0.0 to 1.0)""" + # We consider stability reached when both coefficients have 50 cycles + int_cycles = self.int_cycles + ext_cycles = self.ext_cycles + + if int_cycles == 0 and ext_cycles == 0: + return 0.0 + + # Average of progress for both + confidence_int = min(int_cycles / 50.0, 1.0) + confidence_ext = min(ext_cycles, 50) / 50.0 + + cycle_confidence = (confidence_int + confidence_ext) / 2.0 + + if self.state.consecutive_failures > 0: + failure_penalty = min(self.state.consecutive_failures * 0.15, 0.6) + cycle_confidence = max(0.2, cycle_confidence - failure_penalty) + + return round(cycle_confidence, 2) + + async def start_learning( + self, + coef_int: float = None, + coef_ext: float = None, + reset_data: bool = True, + allow_kint_boost: bool = True, + allow_kext_overshoot: bool = False, + ): + """Start learning, optionally resetting coefficients and learning data. + + Args: + coef_int: Target internal coefficient (defaults to configured value) + coef_ext: Target external coefficient (defaults to configured value) + reset_data: If True, reset all learning data; if False, resume with existing data + allow_kint_boost: Enable Kint boost on stagnation + allow_kext_overshoot: Enable Kext compensation on overshoot + """ + # Update optional flags immediately (even if not resetting data) + self.state.allow_kint_boost = allow_kint_boost + self.state.allow_kext_overshoot = allow_kext_overshoot + _LOGGER.info( + "%s - Auto TPI: Optional parameters set: allow_kint_boost=%s, allow_kext_overshoot=%s", + self._name, allow_kint_boost, allow_kext_overshoot + ) + + # Use provided values, or fallback to default (configured) values + target_int = coef_int if coef_int is not None else self._default_coef_int + target_ext = coef_ext if coef_ext is not None else self._default_coef_ext + + if reset_data: + _LOGGER.info("%s - Auto TPI: Starting learning with coef_int=%.3f, coef_ext=%.3f (resetting all data)", self._name, target_int, target_ext) + + # Reset coefficients to target values + self.state.coeff_indoor_heat = target_int + self.state.coeff_indoor_cool = target_int + self.state.coeff_outdoor_heat = target_ext + self.state.coeff_outdoor_cool = target_ext + + # Reset all counters + self.state.coeff_indoor_autolearn = self._avg_initial_weight + self.state.coeff_outdoor_autolearn = 0 + self.state.coeff_indoor_cool_autolearn = self._avg_initial_weight + self.state.coeff_outdoor_cool_autolearn = 0 + + # Reset all learning data for fresh start + self.state.last_power = 0.0 + self.state.last_order = 0.0 + self.state.last_temp_in = 0.0 + self.state.last_temp_out = 0.0 + self.state.last_state = "stop" + self.state.last_update_date = None + self.state.last_heater_stop_time = None + self.state.total_cycles = 0 + self.state.consecutive_failures = 0 + self.state.last_learning_status = "learning_started" + self.state.cycle_start_date = dt_util.now() + self.state.cycle_active = False + self.state.current_cycle_params = None # Ensure first tick starts fresh + + # Reset capacity if configured heat_rate is 0 (user wants to re-learn capacity) + if self._heating_rate == 0.0: + _LOGGER.info( + "%s - Auto TPI: Configured heat_rate is 0, resetting capacity for bootstrap", + self._name + ) + self.state.max_capacity_heat = 0.0 + self.state.capacity_heat_learn_count = 0 + self.state.bootstrap_failure_count = 0 + else: + # If start_learning is called with explicit target values that differ from defaults, + # apply them as an update to the current state, even without a full reset. + if coef_int is not None and abs(coef_int - self._default_coef_int) > 0.001: + _LOGGER.info("%s - Auto TPI: Updating Kint to %.3f (Manual override in resume)", self._name, target_int) + self.state.coeff_indoor_heat = target_int + self.state.coeff_indoor_cool = target_int + + if coef_ext is not None and abs(coef_ext - self._default_coef_ext) > 0.001: + _LOGGER.info("%s - Auto TPI: Updating Kext to %.3f (Manual override in resume)", self._name, target_ext) + self.state.coeff_outdoor_heat = target_ext + self.state.coeff_outdoor_cool = target_ext + + _LOGGER.info( + "%s - Auto TPI: Resuming learning with existing data (coef_int=%.3f, coef_ext=%.3f, cycles=%d)", + self._name, + self.state.coeff_indoor_heat, + self.state.coeff_outdoor_heat, + self.state.total_cycles, + ) + # Update status to indicate learning has resumed + self.state.last_learning_status = "learning_resumed" + + # Always enable learning when activating + self.state.autolearn_enabled = True + + # Set start date only if it's a new session (reset) or if it wasn't set (first start) + if reset_data or self.state.learning_start_date is None: + self.state.learning_start_date = dt_util.now() + + # ===== BOOTSTRAP PHASE LOGIC ===== + # Determine bootstrap strategy (3 modes) + manual_capacity = self._heating_rate # From config (CONF_AUTO_TPI_HEATING_POWER) + + if manual_capacity > 0: + # Mode 1: Manual capacity provided - skip bootstrap + self.state.max_capacity_heat = manual_capacity + self.state.capacity_heat_learn_count = 3 # Mark as learned + + _LOGGER.info( + "%s - Auto TPI: Using manual capacity %.2f °C/h, skipping bootstrap", + self._name, manual_capacity + ) + + elif self.state.max_capacity_heat > 0 and not reset_data: + # Capacity already learned from previous session - skip bootstrap + + _LOGGER.info( + "%s - Auto TPI: Capacity already known (%.2f °C/h), resuming in TPI mode", + self._name, self.state.max_capacity_heat + ) + + else: + # Mode 2: No manual capacity - Try pre-bootstrap calibration first + calibration_result = await self._try_pre_bootstrap_calibration() + + if calibration_result: + # Pre-calibration succeeded, skip bootstrap + self.state.max_capacity_heat = calibration_result + self.state.capacity_heat_learn_count = 3 # Mark as learned + _LOGGER.info( + "%s - Auto TPI: Pre-bootstrap calibration succeeded (capacity=%.2f °C/h), skipping bootstrap", + self._name, calibration_result + ) + else: + # Pre-calibration failed or insufficient reliability, proceed with bootstrap + # Bootstrap will automatically activate (capacity_heat_learn_count < 3) + # High coefficients will be used during first 3 cycles + _LOGGER.info( + "%s - Auto TPI: Pre-bootstrap calibration failed or insufficient reliability, starting capacity bootstrap (TPI aggressive mode)", + self._name + ) + + # Ensure max_capacity fallback for TPI mode (unchanged) + if self.state.max_capacity_heat == 0.0: + self.state.max_capacity_heat = 1.0 + if self.state.max_capacity_cool == 0.0: + self.state.max_capacity_cool = 1.0 + + await self.async_save_data() + + async def stop_learning(self, save_capacity: bool = True): + _LOGGER.info("%s - Auto TPI: Stopping learning", self._name) + self.state.autolearn_enabled = False + # Do not clear learning_start_date to allow resuming or display in history + # self.state.learning_start_date = None + self.state.last_learning_status = "learning_stopped" + await self.async_save_data() + + # If we have learned enough, save capacity to config + # ONLY if save_capacity is True (avoid double reload if caller handles it) + if save_capacity and self.state.capacity_heat_learn_count >= 3: + # Check if value has changed before saving to avoid useless reload + current_capacity = self._config_entry.data.get(CONF_AUTO_TPI_HEATING_POWER) + if current_capacity is None or abs(current_capacity - self.state.max_capacity_heat) > 0.01: + if self._hass and self._hass.loop and not self._hass.loop.is_closed(): + await self.async_update_learning_data(capacity=self.state.max_capacity_heat, is_heat_mode=True) + + + async def reset_learning_data(self): + _LOGGER.info("%s - Auto TPI: Resetting all learning data", self._name) + self.state = AutoTpiState() + self.state.cycle_active = False + await self.async_save_data() + + async def reset_capacities(self): + """Reset max heat/cool capacities to default (1.0).""" + _LOGGER.info("%s - Auto TPI: Resetting max heat/cool capacities to default (1.0)", self._name) + self.state.max_capacity_heat = 1.0 + self.state.max_capacity_cool = 1.0 + await self.async_save_data() diff --git a/custom_components/versatile_thermostat/base_entity.py b/custom_components/versatile_thermostat/base_entity.py new file mode 100644 index 0000000..c0bb422 --- /dev/null +++ b/custom_components/versatile_thermostat/base_entity.py @@ -0,0 +1,134 @@ +""" A base class for all VTherm entities""" + +import logging +from vtherm_api.log_collector import get_vtherm_logger +from datetime import timedelta +from homeassistant.core import HomeAssistant, callback, Event +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import DOMAIN as CLIMATE_DOMAIN +from homeassistant.const import EntityCategory +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.event import async_track_state_change_event, async_call_later + + +from .const import DOMAIN, DEVICE_MANUFACTURER + +from .base_thermostat import BaseThermostat + +_LOGGER = get_vtherm_logger(__name__) + + +class VersatileThermostatBaseEntity(Entity): + """A base class for all entities""" + + _my_climate: BaseThermostat + hass: HomeAssistant + _config_id: str + _device_name: str + + def __init__(self, hass: HomeAssistant, config_id, device_name) -> None: + """The CTOR""" + self.hass = hass + self._config_id = config_id + self._device_name = device_name + # self._attr_entity_category = EntityCategory.DIAGNOSTIC + self._my_climate = None + self._cancel_call = None + self._attr_has_entity_name = True + + @property + def should_poll(self) -> bool: + """Do not poll for those entities""" + return False + + @property + def my_climate(self) -> BaseThermostat | None: + """Returns my climate if found""" + if not self._my_climate: + self._my_climate = self.find_my_versatile_thermostat() + if self._my_climate: + # Only the first time + self.my_climate_is_initialized() + return self._my_climate + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + entry_type=None, + identifiers={(DOMAIN, self._config_id)}, + name=self._device_name, + manufacturer=DEVICE_MANUFACTURER, + model=DOMAIN, + ) + + def find_my_versatile_thermostat(self) -> BaseThermostat: + """Find the underlying climate entity""" + try: + component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN] + for entity in list(component.entities): + # _LOGGER.debug("Device_info is %s", entity.device_info) + if entity.device_info == self.device_info: + # _LOGGER.debug("Found %s!", entity) + return entity + except KeyError: + pass + + return None + + @callback + async def async_added_to_hass(self): + """Listen to my climate state change""" + + # Check delay condition + async def try_find_climate(_): + _LOGGER.debug( + "%s - Calling VersatileThermostatBaseEntity.async_added_to_hass", self + ) + mcl = self.my_climate + if mcl: + if self._cancel_call: + self._cancel_call() + self._cancel_call = None + self.async_on_remove( + async_track_state_change_event( + self.hass, + [mcl.entity_id], + self.async_my_climate_changed, + ) + ) + else: + _LOGGER.debug("%s - no entity to listen. Try later", self) + self._cancel_call = async_call_later( + self.hass, timedelta(seconds=1), try_find_climate + ) + + await try_find_climate(None) + + @callback + def my_climate_is_initialized(self): + """Called when the associated climate is initialized""" + return + + @callback + async def async_my_climate_changed( + self, event: Event + ): # pylint: disable=unused-argument + """Called when my climate have change + This method aims to be overridden to take the status change + """ + return + + @property + def entity_category(self): + if not self.my_climate: + return EntityCategory.DIAGNOSTIC + return None + + def __str__(self) -> str: + if self._my_climate is not None: + return f"{self._my_climate}-{self.__class__.__name__}" + else: + return f"UnknownVTherm-{self.__class__.__name__}" diff --git a/custom_components/versatile_thermostat/base_manager.py b/custom_components/versatile_thermostat/base_manager.py new file mode 100644 index 0000000..264d808 --- /dev/null +++ b/custom_components/versatile_thermostat/base_manager.py @@ -0,0 +1,71 @@ +""" Implements a base Feature Manager for Versatile Thermostat """ + +from typing import Any + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant + +from vtherm_api.log_collector import get_vtherm_logger + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons_type import ConfigData + +from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import + +_LOGGER = get_vtherm_logger(__name__) + + +class BaseFeatureManager: + """A base class for all feature""" + + def __init__(self, vtherm: Any, hass: HomeAssistant, name: str = None): + """Init of a featureManager""" + self._vtherm = vtherm + self._name = vtherm.name if vtherm else name + self._active_listener: list[CALLBACK_TYPE] = [] + self._hass = hass + + def post_init(self, entry_infos: ConfigData): + """Initialize the attributes of the FeatureManager""" + raise NotImplementedError() + + async def start_listening(self): + """Start listening the underlying entity""" + raise NotImplementedError() + + def stop_listening(self) -> bool: + """stop listening to the sensor""" + while self._active_listener: + self._active_listener.pop()() + + self._active_listener = [] + + async def refresh_state(self): + """Refresh the state and return True if a change have been made""" + return False + + def add_listener(self, func: CALLBACK_TYPE) -> None: + """Add a listener to the list of active listener""" + self._active_listener.append(func) + + def restore_state(self, old_state) -> None: + """Restore state from old state.""" + + @property + def is_configured(self) -> bool: + """True if the FeatureManager is fully configured""" + raise NotImplementedError() + + @property + def name(self) -> str: + """The name""" + return self._name if self._name else "Unnamed FeatureManager" + + @property + def hass(self) -> HomeAssistant: + """The HA instance""" + return self._hass + + @property + def is_detected(self) -> bool: + """True if the FeatureManager is detected. Should be implemented by the child class""" + raise NotImplementedError() diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py new file mode 100644 index 0000000..788e2fc --- /dev/null +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -0,0 +1,2272 @@ +# pylint: disable=line-too-long +# pylint: disable=too-many-lines +# pylint: disable=invalid-name +""" Implements the VersatileThermostat climate component """ +import math +from typing import Optional, Any, Generic +from datetime import datetime +from collections.abc import Callable + +from vtherm_api.log_collector import get_vtherm_logger +from homeassistant.util import dt as dt_util + +from homeassistant.exceptions import ServiceValidationError +from homeassistant.core import ( + HomeAssistant, + callback, + Event, + State, +) +from homeassistant.exceptions import HomeAssistantError + +from homeassistant.components.climate import ClimateEntity +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 + +from homeassistant.helpers.event import ( + async_track_state_change_event, +) + + +from homeassistant.components.climate.const import ( + ATTR_PRESET_MODE, + # ATTR_FAN_MODE, + HVACMode, + HVACAction, + ClimateEntityFeature, +) + +from homeassistant.const import ( + ATTR_TEMPERATURE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons import write_event_log +from .commons_type import ConfigData + +from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import + +from .vtherm_central_api import VersatileThermostatAPI +from .underlyings import UnderlyingEntity, T + +from .ema import ExponentialMovingAverage + +from .base_manager import BaseFeatureManager +from .feature_presence_manager import FeaturePresenceManager +from .feature_power_manager import FeaturePowerManager +from .feature_motion_manager import FeatureMotionManager +from .feature_window_manager import FeatureWindowManager +from .feature_safety_manager import FeatureSafetyManager +from .feature_auto_start_stop_manager import FeatureAutoStartStopManager +from .feature_lock_manager import FeatureLockManager +from .feature_timed_preset_manager import FeatureTimedPresetManager +from .feature_heating_failure_detection_manager import FeatureHeatingFailureDetectionManager +from .feature_repair_incorrect_state_manager import FeatureRepairIncorrectStateManager +from .state_manager import StateManager +from .vtherm_state import VThermState +from .vtherm_preset import VThermPreset, HIDDEN_PRESETS, PRESET_AC_SUFFIX +from .vtherm_hvac_mode import VThermHvacMode, VThermHvacMode_OFF, to_legacy_ha_hvac_mode + +_LOGGER = get_vtherm_logger(__name__) + + +class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): + """Representation of a base class for all Versatile Thermostat device.""" + + # breaking change with 2024.12 climate workaround + _attr_swing_horizontal_modes = [] + _attr_swing_horizontal_mode = "" + + _entity_component_unrecorded_attributes = ( + ClimateEntity._entity_component_unrecorded_attributes.union(frozenset({"configuration", "preset_temperatures"})) + .union(FeaturePresenceManager.unrecorded_attributes) + .union(FeaturePowerManager.unrecorded_attributes) + .union(FeatureMotionManager.unrecorded_attributes) + .union(FeatureWindowManager.unrecorded_attributes) + ) + + ## + ## Startup functions + ## + + def __init__( + self, + hass: HomeAssistant, + unique_id: str, + name: str, + entry_infos: ConfigData, + ): + """Initialize the thermostat.""" + + super().__init__() + + # To remove some silly warning event if code is fixed + self._enable_turn_on_off_backwards_compatibility = False + self._is_removed = False + self._is_startup_done = False + + self._hass = hass + self._entry_infos = None + self._attr_extra_state_attributes = {} + + self._unique_id = unique_id + self._name = name + + self._is_ready: bool = False + + self._cycle_scheduler = None + + # Callbacks for TPI cycle events + self._on_cycle_start_callbacks: list[Callable] = [] + + self._state_manager = StateManager() + self._fan_mode = None + self._humidity = None + self._swing_mode = None + self._swing_horizontal_mode = None + self._ac_mode = None + + self._cur_temp = None + + self._temp_sensor_entity_id = None + self._last_seen_temp_sensor_entity_id = None + self._ext_temp_sensor_entity_id = None + self._last_ext_temperature_measure = None + self._last_temperature_measure = None + self._cur_ext_temp = None + + self._should_relaunch_control_heating = None + + 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 + self._underlying_climate_delta_t = 0 + self._underlying_climate_mean_power_cycle = 0.0 + + self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone) + + # Last change time is the datetime of the last change sent by + # VTherm to the device it is used in `over_climate` when a + # state changes from underlying to avoid loops + self._last_change_time_from_vtherm = None + + self._underlyings: list[T] = [] + + self._ema_temp = None + self._ema_algo = None + self._prop_algorithm = None + self._proportional_function = None + self._tpi_coef_int = None + self._tpi_coef_ext = None + self._minimal_activation_delay = None + self._minimal_deactivation_delay = None + self._tpi_threshold_low = None + self._tpi_threshold_high = None + + self._attr_fan_mode = None + + self._is_central_mode = None + self._last_central_mode = None + self._is_used_by_central_boiler = False + + self._support_flags = None + # Preset will be initialized from Number entities + self._presets: dict[str, Any] = {} # presets + self._presets_away: dict[str, Any] = {} # presets_away + + self._attr_preset_modes: list[str] = [] + self._vtherm_preset_modes: list[VThermPreset] = [] + + self._use_central_config_temperature = False + + self._hvac_off_reason: str | None = None + self._hvac_list: list[VThermHvacMode] = [] + self._str_hvac_list: list[str] = [] + self._temperature_reason: str | None = None + + # Instantiate all features manager + self._managers: list[BaseFeatureManager] = [] + + self._presence_manager: FeaturePresenceManager = FeaturePresenceManager( + self, hass + ) + self._power_manager: FeaturePowerManager = FeaturePowerManager(self, hass) + self._motion_manager: FeatureMotionManager = FeatureMotionManager(self, hass) + self._window_manager: FeatureWindowManager = FeatureWindowManager(self, hass) + self._safety_manager: FeatureSafetyManager = FeatureSafetyManager(self, hass) + # Auto start/stop is only for over_climate + self._auto_start_stop_manager: FeatureAutoStartStopManager | None = None + self._lock_manager: FeatureLockManager = FeatureLockManager(self, hass) + self._timed_preset_manager: FeatureTimedPresetManager = FeatureTimedPresetManager(self, hass) + self._heating_failure_detection_manager: FeatureHeatingFailureDetectionManager = FeatureHeatingFailureDetectionManager(self, hass) + self._repair_incorrect_state_manager: FeatureRepairIncorrectStateManager = FeatureRepairIncorrectStateManager(self, hass) + + self.register_manager(self._presence_manager) + self.register_manager(self._power_manager) + self.register_manager(self._motion_manager) + self.register_manager(self._window_manager) + self.register_manager(self._safety_manager) + self.register_manager(self._lock_manager) + self.register_manager(self._timed_preset_manager) + self.register_manager(self._heating_failure_detection_manager) + self.register_manager(self._repair_incorrect_state_manager) + + self._cancel_recalculate_later: Callable[[], None] | None = None + + self.post_init(entry_infos) + + def register_manager(self, manager: BaseFeatureManager): + """Register a manager""" + self._managers.append(manager) + + def clean_central_config_doublon( + self, config_entry: ConfigData, central_config: ConfigEntry | None + ) -> dict[str, Any]: + """Removes all values from config with are concerned by central_config""" + + def clean_one(cfg, schema: vol.Schema): + """Clean one schema""" + for marker in schema.schema: + # Extract the actual key from Voluptuous Marker objects + key = marker.schema if hasattr(marker, 'schema') else marker + if key in cfg: + del cfg[key] + + cfg = config_entry.copy() + if central_config and central_config.data: + # Removes config if central is used + if cfg.get(CONF_USE_MAIN_CENTRAL_CONFIG) is True: + clean_one(cfg, STEP_CENTRAL_MAIN_DATA_SCHEMA) + + if cfg.get(CONF_USE_TPI_CENTRAL_CONFIG) is True: + clean_one(cfg, STEP_CENTRAL_TPI_DATA_SCHEMA) + + if cfg.get(CONF_USE_WINDOW_CENTRAL_CONFIG) is True: + clean_one(cfg, STEP_CENTRAL_WINDOW_DATA_SCHEMA) + + if cfg.get(CONF_USE_MOTION_CENTRAL_CONFIG) is True: + clean_one(cfg, STEP_CENTRAL_MOTION_DATA_SCHEMA) + + if cfg.get(CONF_USE_POWER_CENTRAL_CONFIG) is True: + clean_one(cfg, STEP_CENTRAL_POWER_DATA_SCHEMA) + + if cfg.get(CONF_USE_PRESENCE_CENTRAL_CONFIG) is True: + clean_one(cfg, STEP_CENTRAL_PRESENCE_DATA_SCHEMA) + + if cfg.get(CONF_USE_ADVANCED_CENTRAL_CONFIG) is True: + clean_one(cfg, STEP_CENTRAL_ADVANCED_DATA_SCHEMA) + + if cfg.get(CONF_USE_LOCK_CENTRAL_CONFIG) is True: + clean_one(cfg, STEP_CENTRAL_LOCK_DATA_SCHEMA) + + # take all central config + entry_infos = central_config.data.copy() + # and merge with cleaned config_entry + entry_infos.update(cfg) + else: + entry_infos = cfg + + return entry_infos + + def post_init(self, config_entry: ConfigData): + """Finish the initialization of the thermostat""" + + _LOGGER.info( + "%s - Updating VersatileThermostat with infos %s", + self, + config_entry, + ) + + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass) + central_config = api.find_central_configuration() + + entry_infos = self.clean_central_config_doublon(config_entry, central_config) + + _LOGGER.info("%s - The merged configuration is %s", self, entry_infos) + + self._entry_infos = entry_infos + + # Post init all managers + for manager in self._managers: + manager.post_init(entry_infos) + + self._use_central_config_temperature = entry_infos.get( + CONF_USE_PRESETS_CENTRAL_CONFIG + ) or ( + entry_infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG) + and entry_infos.get(CONF_USE_PRESENCE_FEATURE) + ) + + self._ac_mode = entry_infos.get(CONF_AC_MODE) is True + self._attr_max_temp = float(entry_infos.get(CONF_TEMP_MAX, 0.0)) + self._attr_min_temp = float(entry_infos.get(CONF_TEMP_MIN, 0.0)) + if (step := entry_infos.get(CONF_STEP_TEMPERATURE)) is not None: + self._attr_target_temperature_step = step + + self._attr_preset_modes = [] + self._vtherm_preset_modes = [] + + self._cycle_min = max(1, entry_infos.get(CONF_CYCLE_MIN, 1)) + + # Initialize underlying entities (will be done in subclasses) + self._underlyings = [] + + self._temp_sensor_entity_id = entry_infos.get(CONF_TEMP_SENSOR) + self._last_seen_temp_sensor_entity_id = entry_infos.get( + CONF_LAST_SEEN_TEMP_SENSOR + ) + self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR) + + self.set_hvac_list() + + self._unit = self._hass.config.units.temperature_unit + + # Will be restored if possible + self._state_manager = StateManager() + + self._support_flags = SUPPORT_FLAGS + + # Preset will be initialized from Number entities + self._presets: dict[str, Any] = {} # presets + self._presets_away: dict[str, Any] = {} # presets_away + + self._humidity = None + self._fan_mode = None + self._swing_mode = None + self._swing_horizontal_mode = None + self._cur_temp = None + self._cur_ext_temp = None + + self._last_temperature_measure = self.now + self._last_ext_temperature_measure = self.now + + 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 + if api and api.short_ema_params: + short_ema_params = api.short_ema_params + + self._ema_algo = ExponentialMovingAverage( + self.name, + short_ema_params.get("halflife_sec"), + # Needed for time calculation + get_tz(self._hass), + # two digits after the coma for temperature slope calculation + short_ema_params.get("precision"), + short_ema_params.get("max_alpha"), + ) + + self._is_central_mode = not ( + entry_infos.get(CONF_USE_CENTRAL_MODE) is False + ) # Default value (None) is True + + self._is_used_by_central_boiler = ( + entry_infos.get(CONF_USED_BY_CENTRAL_BOILER) is True + ) + + self._max_on_percent = api.max_on_percent + + _LOGGER.debug( + "%s - Creation of a new VersatileThermostat entity: unique_id=%s", + self, + self.unique_id, + ) + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + _LOGGER.debug("%s - Calling async_added_to_hass", self) + + await super().async_added_to_hass() + + self.async_on_remove( + async_track_state_change_event( + self.hass, + [self._temp_sensor_entity_id], + self._async_temperature_changed, + ) + ) + + if self._last_seen_temp_sensor_entity_id: + self.async_on_remove( + async_track_state_change_event( + self.hass, + [self._last_seen_temp_sensor_entity_id], + self._async_last_seen_temperature_changed, + ) + ) + + if self._ext_temp_sensor_entity_id: + self.async_on_remove( + async_track_state_change_event( + self.hass, + [self._ext_temp_sensor_entity_id], + self._async_ext_temperature_changed, + ) + ) + + self.async_on_remove(self.remove_thermostat) + + # 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""" + self._is_removed = True + _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) + + self.stop_recalculate_later() + + # Cancel scheduler timers so leftover async_call_later handles cannot fire + # after the entity has been unloaded (e.g. on cycle duration config change). + if self._cycle_scheduler: + self._cycle_scheduler.shutdown() + + # stop listening for all managers + for manager in self._managers: + manager.stop_listening() + + for under in self._underlyings: + under.remove_entity() + + def register_cycle_callback( + self, + on_start: Callable | None = None, + ): + """Register callbacks for TPI cycle events. + + Args: + on_start: Callback called at the start of each TPI cycle + Signature: async def callback(on_time_sec, off_time_sec, on_percent, hvac_mode) + """ + if on_start: + self._on_cycle_start_callbacks.append(on_start) + _LOGGER.debug("%s - Registered cycle start callback: %s", self, on_start) + # Register on the CycleScheduler if available + if self._cycle_scheduler: + self._cycle_scheduler.register_cycle_start_callback(on_start) + + def stop_recalculate_later(self): + """Stop any scheduled call later tasks if any.""" + if self._cancel_recalculate_later: + self._cancel_recalculate_later() # pylint: disable=not-callable + self._cancel_recalculate_later = None + + async def async_startup(self, central_configuration): + """Triggered on startup, used to get old state and set internal states + accordingly. This is triggered by VTherm API""" + write_event_log(_LOGGER, self, "Start up VTherm") + + _LOGGER.debug("%s - Calling async_startup_internal", self) + # need_write_state = False + + # start listening for all managers + for manager in self._managers: + await manager.start_listening() + + await self.get_my_previous_state() + + # Start underlyings and register cycle callbacks on the scheduler + for under in self._underlyings: + under.startup() + if self._cycle_scheduler: + for cb in self._on_cycle_start_callbacks: + self._cycle_scheduler.register_cycle_start_callback(cb) + + # init presets. Should be after underlyings init because for over_climate it uses the hvac_modes + await self.init_presets(central_configuration) + + temperature_state = self.hass.states.get(self._temp_sensor_entity_id) + if temperature_state and temperature_state.state not in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + _LOGGER.debug( + "%s - temperature sensor have been retrieved: %.1f", + self, + float(temperature_state.state), + ) + await self._async_update_temp(temperature_state) + + if self._ext_temp_sensor_entity_id: + ext_temperature_state = self.hass.states.get( + self._ext_temp_sensor_entity_id + ) + if ext_temperature_state and ext_temperature_state.state not in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + _LOGGER.debug( + "%s - external temperature sensor have been retrieved: %.1f", + self, + float(ext_temperature_state.state), + ) + await self._async_update_ext_temp(ext_temperature_state) + else: + _LOGGER.debug( + "%s - external temperature sensor have NOT been retrieved " + "cause unknown or unavailable", + self, + ) + else: + _LOGGER.debug( + "%s - external temperature sensor have NOT been retrieved " + "cause no external sensor", + self, + ) + self._is_startup_done = True + + if self.is_ready: + await self.init_underlyings_completed() + + async def init_underlyings_completed(self, under_entity_id: Optional[str] = None): + """All underlyings have been initialized. Then we can finish our initialization""" + _LOGGER.debug("%s - Calling init_underlyings_completed", self) + if not self.is_ready: + return + + # We: + # - refresh all managers states, + # - calculate the current state of the VTherm (it depends on the managers states and the requested state) + # - check if the initial conditions are met + # - force the first cycle if changes has been detected + + # in over_climate, we have to remove the FROST preset for a COOL only device + if self._ac_mode and VThermPreset.FROST in self._vtherm_preset_modes and HVACMode.HEAT not in self.hvac_modes: + self._vtherm_preset_modes.remove(VThermPreset.FROST) + if VThermPreset.FROST in self._attr_preset_modes: + self._attr_preset_modes.remove(VThermPreset.FROST) + _LOGGER.debug( + "%s - removed FROST preset for COOL only device, new presets: %s", + self, + self._attr_preset_modes, + ) + + # refresh states for all managers + for manager in self._managers: + await manager.refresh_state() + # need_write_state = True + + await self.update_states(force=True) + self.recalculate() + + # check initial state should be done after the current state has been calculated and so after the manager has been updated + # issue 1654 - initial state check should be done after the underlyings has come to life + # await self._check_initial_state() + self.reset_last_change_time_from_vtherm() + + def restore_specific_previous_state(self, old_state: State): + """Should be overridden in each specific thermostat + if a specific previous state or attribute should be + restored + """ + + async def get_my_previous_state(self): + """Try to get my previous state""" + # Check If we have an old state + old_state = await self.async_get_last_state() + _LOGGER.debug( + "%s - Calling get_my_previous_state old_state is %s", self, old_state + ) + if old_state is not None: + # Restore current_state + if current_state_attr := old_state.attributes.get(ATTR_CURRENT_STATE, None): + current_state = VThermState.from_dict(current_state_attr) + self._state_manager.current_state.set_state( + hvac_mode=current_state.hvac_mode, + target_temperature=current_state.target_temperature, + preset=current_state.preset, + ) + else: + # Try to init current_state with old temperature, preset and mode + self._state_manager.current_state.set_state( + target_temperature=old_state.attributes.get(ATTR_TEMPERATURE, None), + preset=VThermPreset(old_state.attributes.get(ATTR_PRESET_MODE, None) or VThermPreset.NONE), + hvac_mode=old_state.state if isinstance(old_state.state, VThermHvacMode) else from_ha_hvac_mode(old_state.state), + ) + # If we have no initial temperature set with min (or max depending on ac_mode) + if self._state_manager.current_state.target_temperature is None: + self._state_manager.current_state.set_target_temperature(self.max_temp if self._ac_mode else self.min_temp) + _LOGGER.warning( + "%s - Undefined target temperature, falling back to %s", + self, + self._state_manager.current_state.target_temperature, + ) + + # Restore requested_state or default with current_state + if requested_state_attr := old_state.attributes.get(ATTR_REQUESTED_STATE, None): + requested_state = VThermState.from_dict(requested_state_attr) + self._state_manager.requested_state.set_state( + hvac_mode=requested_state.hvac_mode, + target_temperature=requested_state.target_temperature, + preset=requested_state.preset, + ) + else: + # Try to init requested_state with old temperature, preset and mode + self._state_manager.requested_state.set_state( + target_temperature=old_state.attributes.get(ATTR_TEMPERATURE, None), + preset=VThermPreset(old_state.attributes.get(ATTR_PRESET_MODE, None) or VThermPreset.NONE), + hvac_mode=old_state.state if isinstance(old_state.state, VThermHvacMode) else from_ha_hvac_mode(old_state.state), + ) + + self._hvac_off_reason = old_state.attributes.get(HVAC_OFF_REASON_NAME, None) + + # Try to get total_energy from specific_states (new format) or root level (old format) + specific_states = old_state.attributes.get("specific_states", {}) + old_total_energy = specific_states.get(ATTR_TOTAL_ENERGY) + if old_total_energy is None: + # Fallback to root level for backward compatibility + old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY) + self._total_energy = old_total_energy if old_total_energy is not None else 0 + _LOGGER.debug( + "%s - get_my_previous_state restored energy is %s", + self, + self._total_energy, + ) + + self.restore_specific_previous_state(old_state) + + # Restore all managers states from previous state + for manager in self._managers: + manager.restore_state(old_state) + else: + # No previous state, try and restore defaults + if self._state_manager.current_state.target_temperature is None: + self._state_manager.current_state.set_target_temperature(self.max_temp if self._ac_mode else self.min_temp) + _LOGGER.warning("No previously saved temperature, setting to %s", self._state_manager.current_state.target_temperature) + self._total_energy = 0 + _LOGGER.debug( + "%s - get_my_previous_state no previous state energy is %s", + self, + self._total_energy, + ) + + if not self.is_on and self.hvac_off_reason is None: + self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL) + + _LOGGER.info( + "%s - restored current state is %s, requested state is %s", + self, + self._state_manager.current_state, + self._state_manager.requested_state, + ) + + def __str__(self) -> str: + return self.name + + def set_hvac_list(self): + """Set the hvac list depending on ac_mode""" + self._hvac_list = self.build_hvac_list() + self._str_hvac_list = [to_ha_hvac_mode(mode) for mode in self._hvac_list if to_ha_hvac_mode(mode) is not None] + + @callback + def async_registry_entry_updated(self): + """update the entity if the config entry have been updated + Note: this don't work either + """ + write_event_log(_LOGGER, self, "The config entry have been updated") + + @callback + async def entry_update_listener(self, _, config_entry: ConfigEntry) -> None: # hass: HomeAssistant, + """Called when the entry have changed in ConfigFlow""" + _LOGGER.info("%s - Change entry with the values: %s", self, config_entry.data) + + async def init_presets(self, central_config): + """Init all presets of the VTherm""" + # If preset central config is used and central config is set, + # take the presets from central config + vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api() + + presets: dict[str, Any] = {} + presets_away: dict[str, Any] = {} + + def calculate_presets(items, use_central_conf_key): + presets: dict[str, Any] = {} + config_id = self._unique_id + if central_config and self._entry_infos.get(use_central_conf_key, False) is True: + config_id = central_config.entry_id + + for key, preset_name in items: + _LOGGER.debug("%s - looking for key=%s, preset_name=%s", self, key, preset_name) + # removes preset_name frost if heat is not in hvac_modes. vtherm_hvac_modes is initialized when the underlyings are initialized. + # So it may be not be ready yet here. In that case, the FROST is added anyway. So it is possible that FROST preset in a COOL only device + if len(self.vtherm_hvac_modes) == 0 and key == VThermPreset.FROST and VThermHvacMode_HEAT not in self.vtherm_hvac_modes: + _LOGGER.debug("%s - removing preset_name %s which reserved for HEAT devices", self, preset_name) + continue + value = vtherm_api.get_temperature_number_value(config_id=config_id, preset_name=preset_name) + if value is not None: + presets[key] = value + else: + _LOGGER.debug("%s - preset_name %s not found in VTherm API", self, preset_name) + presets[key] = self._attr_max_temp if self._ac_mode else self._attr_min_temp + return presets + + # Calculate all presets + presets = calculate_presets( + CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items(), + CONF_USE_PRESETS_CENTRAL_CONFIG, + ) + + # refacto + # if self._entry_infos.get(CONF_USE_PRESENCE_FEATURE) is True: + if self._presence_manager.is_configured: + presets_away = calculate_presets( + (CONF_PRESETS_AWAY_WITH_AC.items() if self._ac_mode else CONF_PRESETS_AWAY.items()), + CONF_USE_PRESETS_CENTRAL_CONFIG, + ) + + # aggregate all available presets now + self._presets: dict[str, Any] = presets + self._presets_away: dict[str, Any] = presets_away + + # Calculate all possible presets + self._attr_preset_modes = [VThermPreset.NONE] + if len(self._presets): + self._support_flags = SUPPORT_FLAGS | ClimateEntityFeature.PRESET_MODE + + for key, _ in CONF_PRESETS.items(): + preset_value = self.find_preset_temp(key) + if preset_value is not None and preset_value > 0: + self._attr_preset_modes.append(key) + + _LOGGER.debug("%s - After adding presets, preset_modes to %s", self, self._attr_preset_modes) + else: + _LOGGER.debug("%s - No preset_modes", self) + + if self._motion_manager.is_configured: + self._attr_preset_modes.append(VThermPreset.ACTIVITY) + + # transform _attr_preset_modes into _vtherm_preset_modes + self._vtherm_preset_modes = [VThermPreset(mode) for mode in self._attr_preset_modes] + + # Re-applicate the last preset if any to take change into account + if self._state_manager.current_state.preset and self._state_manager.current_state.preset != VThermPreset.NONE: + await self.async_set_preset_mode_internal(self._state_manager.current_state.preset) + + ## + ## Properties overriden from ClimateEntity (or not) + ## + + @property + def is_initialized(self) -> bool: + """Check if all underlyings are initialized + This is useful only for over_climate in which we + should have found the underlying climate to be operational""" + for under in self._underlyings: + if not under.is_initialized: + return False + return True + + @property + def is_ready(self) -> bool: + """Check if all underlyings are ready (initialized and startup is complete)""" + if not self._is_ready: + self._is_ready = self._is_startup_done and self.is_initialized + return self._is_ready + + @property + def vtherm_hvac_modes(self) -> list[VThermHvacMode]: + """List of available VTherm operation modes.""" + return self._hvac_list + + @property + def hvac_modes(self) -> list[str]: + """List of available VTherm operation modes.""" + return self._str_hvac_list + + def build_hvac_list(self) -> list[VThermHvacMode]: + """Build the hvac list depending on ac_mode""" + if self._ac_mode: + return [VThermHvacMode_HEAT, VThermHvacMode_COOL, VThermHvacMode_OFF] + else: + return [VThermHvacMode_HEAT, VThermHvacMode_OFF] + + @property + def is_over_climate(self) -> bool: + """True if the Thermostat is over_climate""" + return False + + @property + def is_over_switch(self) -> bool: + """True if the Thermostat is over_switch""" + return False + + @property + def has_prop(self) -> bool: + """True if the Thermostat uses a proportional algorithm (TPI, SmartPI)""" + return False + + @property + def safe_on_percent(self) -> float: + """Return the on_percent safe value""" + return 0 + + @property + def is_over_valve(self) -> bool: + """True if the Thermostat is over_valve""" + return False + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + entry_type=None, + identifiers={(DOMAIN, self._unique_id)}, + name=self._name, + manufacturer=DEVICE_MANUFACTURER, + model=DOMAIN, + ) + + @property + def unique_id(self) -> str: + return self._unique_id + + @property + def should_poll(self) -> bool: + return False + + @property + def name(self) -> str: + return self._name + + @property + def ac_mode(self) -> bool | None: + """Get the ac_mode of the Themostat""" + return self._ac_mode + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return self._unit + + @property + def ema_temperature(self) -> float | None: + """Return the EMA temperature.""" + return self._ema_temp + + @property + def hvac_mode(self) -> HVACMode | None: + """Return current operation.""" + return to_legacy_ha_hvac_mode(self._state_manager.current_state.hvac_mode) + + @property + def vtherm_hvac_mode(self) -> VThermHvacMode | None: + """Return current operation.""" + return self._state_manager.current_state.hvac_mode + + @property + def is_recalculate_scheduled(self) -> bool: + """Return true if a recalculation is scheduled.""" + return self._cancel_recalculate_later is not None + + @property + def is_used_by_central_boiler(self) -> HVACAction | None: + """Return true is the VTherm is configured to be used by + central boiler""" + return self._is_used_by_central_boiler + + @property + def target_temperature(self) -> float | None: + """Return the temperature we try to reach.""" + return self._state_manager.current_state.target_temperature + + @property + def supported_features(self) -> ClimateEntityFeature: + """Return the list of supported features.""" + return self._support_flags + + @property + def should_device_be_active(self) -> bool: + """Returns true if one underlying is active""" + for under in self._underlyings: + if under.should_device_be_active: + return True + return False + + @property + def is_device_active(self) -> bool: + """Returns true if one underlying is active""" + for under in self._underlyings: + if under.is_device_active: + return True + return False + + @property + def device_actives(self) -> int: + """Calculate the active devices""" + ret = [] + for under in self._underlyings: + if under.is_device_active: + ret.append(under.entity_id) + return ret + + @property + def nb_device_actives(self) -> int: + """Calculate the number of active devices""" + return len(self.device_actives) + + @property + def current_temperature(self) -> float | None: + """Return the sensor temperature.""" + return self._cur_temp + + @property + def current_outdoor_temperature(self) -> float | None: + """Return the outdoor sensor temperature.""" + return self._cur_ext_temp + + @property + def total_energy(self) -> float | None: + """Returns the total energy calculated for this thermostat""" + if self._total_energy is not None: + return round(self._total_energy, 2) + else: + return None + + @property + def overpowering_state(self) -> bool | None: + """Get the overpowering_state""" + return self._power_manager.overpowering_state + + @property + def power_manager(self) -> FeaturePowerManager | None: + """Get the power manager""" + return self._power_manager + + @property + def is_overpowering_detected(self) -> bool: + """Return True when power shedding is currently active.""" + return ( + self._power_manager.is_overpowering_detected + if self._power_manager is not None + else False + ) + + @property + def presence_manager(self) -> FeaturePresenceManager | None: + """Get the presence manager""" + return self._presence_manager + + @property + def motion_manager(self) -> FeatureMotionManager | None: + """Get the motion manager""" + return self._motion_manager + + @property + def window_manager(self) -> FeatureWindowManager | None: + """Get the window manager""" + return self._window_manager + + @property + def safety_manager(self) -> FeatureSafetyManager | None: + """Get the safety manager""" + return self._safety_manager + + @property + def auto_start_stop_manager(self) -> FeatureAutoStartStopManager | None: + """Get the auto start/stop manager (only implemented in over_climate)""" + return self._auto_start_stop_manager + + @property + def lock_manager(self) -> FeatureLockManager | None: + """Get the lock manager""" + return self._lock_manager + + @property + def timed_preset_manager(self) -> FeatureTimedPresetManager: + """Get the timed preset manager""" + return self._timed_preset_manager + + @property + def heating_failure_detection_manager(self) -> FeatureHeatingFailureDetectionManager: + """Get the heating failure detection manager""" + return self._heating_failure_detection_manager + + @property + def repair_incorrect_state_manager(self) -> FeatureRepairIncorrectStateManager: + """Get the repair incorrect state manager""" + return self._repair_incorrect_state_manager + + @property + def current_state(self) -> VThermState | None: + """Get the current state""" + return self._state_manager.current_state + + @property + def requested_state(self) -> VThermState | None: + """Get the requested state""" + return self._state_manager.requested_state + + @property + def window_state(self) -> str | None: + """Get the window_state""" + return self._window_manager.window_state + + @property + def window_auto_state(self) -> str | None: + """Get the window_auto_state""" + return self._window_manager.window_auto_state + + @property + def is_window_bypass(self) -> bool | None: + """Get the Window Bypass""" + return self._window_manager.is_window_bypass + + @property + def safety_state(self) -> str | None: + """Get the safety_state""" + return self._safety_manager.safety_state + + @property + def motion_state(self) -> str | None: + """Get the motion_state""" + return self._motion_manager.motion_state + + @property + def presence_state(self) -> str | None: + """Get the presence_state""" + return self._presence_manager.presence_state + + # ========================================================================= + # PUBLIC PROPERTIES FOR HANDLER ACCESS + # These properties expose internal state to handlers without breaking encapsulation + # ========================================================================= + + @property + def cycle_min(self) -> int: + """Return the cycle duration in minutes.""" + return self._cycle_min + + @property + def entry_infos(self) -> ConfigData: + """Return the configuration entry data.""" + return self._entry_infos + + @property + def hass(self) -> HomeAssistant: + """Return the Home Assistant instance. + + This overrides the Entity.hass property to return our stored _hass + reference during initialization (before async_added_to_hass is called). + """ + return self._hass + + @hass.setter + def hass(self, value: HomeAssistant): + """Set the Home Assistant instance. + + This is called by the entity platform when adding the entity. + """ + self._hass = value + + @property + def is_removed(self) -> bool: + """Return True if the thermostat has been removed.""" + return self._is_removed + + @property + def ext_temp_sensor_entity_id(self) -> str | None: + """Return the external temperature sensor entity ID.""" + return self._ext_temp_sensor_entity_id + + @property + def max_on_percent(self) -> float | None: + """Return the maximum on percentage.""" + return self._max_on_percent + + @property + def tpi_coef_int(self) -> float | None: + """Return the TPI internal coefficient.""" + return self._tpi_coef_int + + @tpi_coef_int.setter + def tpi_coef_int(self, value: float): + """Set the TPI internal coefficient.""" + self._tpi_coef_int = value + + @property + def tpi_coef_ext(self) -> float | None: + """Return the TPI external coefficient.""" + return self._tpi_coef_ext + + @tpi_coef_ext.setter + def tpi_coef_ext(self, value: float): + """Set the TPI external coefficient.""" + self._tpi_coef_ext = value + + @property + def tpi_threshold_low(self) -> float: + """Return the TPI low threshold.""" + return self._tpi_threshold_low or 0.0 + + @tpi_threshold_low.setter + def tpi_threshold_low(self, value: float): + """Set the TPI low threshold.""" + self._tpi_threshold_low = value + + @property + def tpi_threshold_high(self) -> float: + """Return the TPI high threshold.""" + return self._tpi_threshold_high or 0.0 + + @tpi_threshold_high.setter + def tpi_threshold_high(self, value: float): + """Set the TPI high threshold.""" + self._tpi_threshold_high = value + + @property + def minimal_activation_delay(self) -> int: + """Return the minimal activation delay in seconds.""" + return self._minimal_activation_delay or 0 + + @minimal_activation_delay.setter + def minimal_activation_delay(self, value: int): + """Set the minimal activation delay in seconds.""" + self._minimal_activation_delay = value + + @property + def minimal_deactivation_delay(self) -> int: + """Return the minimal deactivation delay in seconds.""" + return self._minimal_deactivation_delay or 0 + + @minimal_deactivation_delay.setter + def minimal_deactivation_delay(self, value: int): + """Set the minimal deactivation delay in seconds.""" + self._minimal_deactivation_delay = value + + @property + def proportional_function(self) -> str | None: + """Return the proportional function type (TPI, SmartPI).""" + return self._proportional_function + + @property + def prop_algorithm(self): + """Return the proportional algorithm instance.""" + return self._prop_algorithm + + @prop_algorithm.setter + def prop_algorithm(self, value): + """Set the proportional algorithm instance.""" + self._prop_algorithm = value + + @property + def cycle_scheduler(self): + """Return the CycleScheduler instance, if any.""" + return self._cycle_scheduler + + @property + def underlyings(self) -> list: + """Return the list of underlying entities.""" + return self._underlyings + + @property + def proportional_algorithm(self): + """Get the eventual ProportionalAlgorithm""" + return None + + @property + def last_temperature_measure(self) -> datetime | None: + """Get the last temperature datetime""" + return self._last_temperature_measure + + @property + def last_ext_temperature_measure(self) -> datetime | None: + """Get the last external temperature datetime""" + return self._last_ext_temperature_measure + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode comfort, eco, boost,...,""" + return str(self._state_manager.current_state.preset) + + @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 vtherm_preset_mode(self) -> VThermPreset | None: + """Return the current preset mode comfort, eco, boost,...,""" + return self._state_manager.current_state.preset + + @property + def vtherm_preset_modes(self) -> list[str] | None: + """Return a list of available preset modes. + Requires ClimateEntityFeature.PRESET_MODE. + """ + return self._vtherm_preset_modes + + @property + def last_temperature_slope(self) -> float | None: + """Return the last temperature slope curve if any""" + return self._window_manager.last_slope + + @property + def nb_underlying_entities(self) -> int: + """Returns the number of underlying entities""" + return len(self._underlyings) + + @property + def underlying_entities(self) -> list | None: + """Returns the underlying entities""" + return self._underlyings + + @property + def activable_underlying_entities(self) -> list | None: + """Returns the activable underlying entities for controlling + the central boiler""" + return self.underlying_entities + + @property + def all_underlying_entities(self) -> list | None: + """Returns all underlying entities for controling the central boiler""" + return self.activable_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: + if under.entity_id == entity_id: + return under + return None + + @property + def is_on(self) -> bool: + """True if the VTherm is on (! HVAC_OFF)""" + return self.vtherm_hvac_mode and self.vtherm_hvac_mode != VThermHvacMode_OFF + + @property + def is_controlled_by_central_mode(self) -> bool: + """Returns True if this VTherm can be controlled by the central_mode""" + return self._is_central_mode + + @property + def last_central_mode(self) -> str | None: + """Returns the last central_mode taken into account. + Is None if the VTherm is not controlled by central_mode""" + return self._last_central_mode + + @property + def use_central_config_temperature(self): + """True if this VTHerm uses the central configuration temperature""" + return self._use_central_config_temperature + + @property + def hvac_off_reason(self) -> str | None: + """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 + + @property + def temperature_reason(self) -> str | None: + """Returns the reason of the target temperature + This is useful for features that changes the VTherm like window detection or power management""" + return self._temperature_reason + + @property + def is_sleeping(self) -> bool: + """True if the thermostat is in sleep mode. Only for over_climate with valve regulation""" + return False + + @property + def have_valve_regulation(self) -> bool: + """True if the Thermostat is regulated by valve""" + return False + + @property + def power_percent(self) -> float | None: + """Get the current on_percent as a percentage value. valid only for Vtherm with a TPI algo + Get the current on_percent value""" + prop_algo = getattr(self, '_prop_algorithm', None) + if prop_algo and prop_algo.on_percent is not None: + return round(prop_algo.on_percent * 100, 0) + else: + return None + + @property + def on_percent(self) -> float | None: + """Get the current on_percent value. valid only for Vtherm with a TPI algo""" + prop_algo = getattr(self, '_prop_algorithm', None) + if prop_algo and prop_algo.on_percent is not None: + return prop_algo.on_percent + else: + return None + + @property + def vtherm_type(self) -> str | None: + """Return the type of thermostat""" + return None + + @property + def config_entry(self) -> ConfigEntry | None: + """Return the config entry associated with the thermostat.""" + return self._hass.config_entries.async_get_entry(self.unique_id) + + def underlying_entity_id(self, index=0) -> str | None: + """The climate_entity_id. Added for retrocompatibility reason""" + if index < self.nb_underlying_entities: + return self.underlying_entity(index).entity_id + else: + return None + + def underlying_entity(self, index=0) -> UnderlyingEntity | None: + """Get the underlying entity at specified index""" + if index < self.nb_underlying_entities: + return self._underlyings[index] + else: + return None + + def turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + raise NotImplementedError() + + @overrides + async def async_turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + raise NotImplementedError() + + @overrides + def turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + raise NotImplementedError() + + @overrides + async def async_turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + raise NotImplementedError() + + ## + ## Entry events (from linked devices or users) + ## + + @staticmethod + def check_lock(func): + """Decorator to check if the thermostat is locked.""" + async def wrapper(self, *args, **kwargs): + if self.lock_manager.check_is_locked(func.__name__): + return + return await func(self, *args, **kwargs) + return wrapper + + @overrides + @check_lock + async def async_set_hvac_mode(self, hvac_mode: VThermHvacMode): # , need_control_heating=True): + """Set new target hvac mode. Uses the HA VThermHvacMode enum to respect the original type.""" + write_event_log(_LOGGER, self, f"Set hvac mode: {hvac_mode}") + + if hvac_mode is None: + return + + # Change the requested state + self._state_manager.requested_state.set_hvac_mode(hvac_mode) + await self.update_states(force=False) + + @overrides + @check_lock + async def async_set_preset_mode(self, preset_mode: str): + """Set new preset mode.""" + # We accept a new preset when: + # 1. last_central_mode is not set, + # 2. or last_central_mode is AUTO, + # 3. or last_central_mode is CENTRAL_MODE_FROST_PROTECTION and preset_mode is + # VThermPreset.FROST (to be abel to re-set the preset_mode) + + write_event_log(_LOGGER, self, f"Set preset mode: {preset_mode}") + + vtherm_preset_mode = VThermPreset(preset_mode) + accept = self._last_central_mode in [ + None, + CENTRAL_MODE_AUTO, + CENTRAL_MODE_COOL_ONLY, + CENTRAL_MODE_HEAT_ONLY, + CENTRAL_MODE_STOPPED, + ] or (self._last_central_mode == CENTRAL_MODE_FROST_PROTECTION and preset_mode == VThermPreset.FROST) + if not accept: + _LOGGER.info( + "%s - Impossible to change the preset to %s because central mode is %s", + self, + preset_mode, + self._last_central_mode, + ) + + return + + await self.async_set_preset_mode_internal(vtherm_preset_mode) # overwrite_saved_preset=overwrite_saved_preset) + await self.update_states(force=True) + + async def async_set_preset_mode_internal(self, preset_mode: VThermPreset): # , overwrite_saved_preset=True): + """Set new preset mode.""" + _LOGGER.info("%s - Set requested preset_mode: %s", self, preset_mode) + + if preset_mode not in (self._vtherm_preset_modes or []) and preset_mode not in HIDDEN_PRESETS: + raise ValueError(f"Got unsupported preset_mode {preset_mode}. Must be one of {self._vtherm_preset_modes}") + + self._state_manager.requested_state.set_preset(preset_mode) + + async def async_set_fan_mode(self, fan_mode: str): + """Set new target fan mode.""" + _LOGGER.info("%s - Set fan mode: %s", self, fan_mode) + return + + async def async_set_humidity(self, humidity: int): + """Set new target humidity.""" + write_event_log(_LOGGER, self, f"Set humidity: {humidity}") + return + + @check_lock + async def async_set_temperature(self, **kwargs): + """Set new requested target temperature and turn off any active presets.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + write_event_log(_LOGGER, self, f"Set target temp: {temperature}") + if temperature is None: + return + + self._state_manager.requested_state.set_target_temperature(temperature) + self._state_manager.requested_state.set_preset(VThermPreset.NONE) + await self.update_states(force=True) + + async def set_preset_temperature( + self, + preset: str, + temperature: float | None = None, + temperature_away: float | None = None, + ): + """Called by a to change the temperature of a preset + data: + preset: boost + temperature: 17.8 + temperature_away: 15 + target: + entity_id: climate.thermostat_2 + """ + write_event_log(_LOGGER, self, f"Calling SERVICE_SET_PRESET_TEMPERATURE, preset: {preset}, temperature: {temperature}, temperature_away: {temperature_away}") + + if preset in self._presets: + if temperature is not None: + self._presets[preset] = temperature + if self._presence_manager.is_configured and temperature_away is not None: + self._presets_away[self.get_preset_away_name(preset)] = temperature_away + else: + _LOGGER.warning("%s - No preset %s configured for this thermostat. ", self, preset) + + if preset in self._presets: + if temperature is not None: + self._presets[preset] = temperature + if self._presence_manager.is_configured and temperature_away is not None: + self._presets_away[self.get_preset_away_name(preset)] = temperature_away + else: + _LOGGER.warning( + "%s - No preset %s configured for this thermostat. Ignoring set_preset_temperature call", + self, + preset, + ) + + # If the changed preset is active, change the current temperature + # Issue #119 - reload new preset temperature also in ac mode + is_active_preset = preset.startswith(self.preset_mode) + + # Also check if activity is the current preset and the changed preset + # is the current motion sub-preset (e.g. comfort or boost) + if not is_active_preset and self.preset_mode == VThermPreset.ACTIVITY and self._motion_manager.is_configured: + current_motion_preset = self._motion_manager.get_current_motion_preset() + is_active_preset = preset.startswith(current_motion_preset) + + if is_active_preset: + self.requested_state.force_changed() + await self.update_states(force=True) + + ## + ## Calculation and utility functions + ## + async def update_states(self, force=False): + """Update the states of the thermostat considering the requested state and the current state""" + if not self.is_ready: + _LOGGER.debug("%s - update_states is called but the entity is not initialized yet. Skip the update", self) + return False + + changed = False + if self._state_manager.requested_state.is_changed: + if changed := await self._state_manager.calculate_current_state(self): + _LOGGER.info("%s - current state changed to %s", self, self._state_manager.current_state) + sub_need_control_heating = False + # Apply preset + if self._state_manager.current_state.is_preset_changed: + _LOGGER.info("%s - Applying new preset: %s", self, self.vtherm_preset_mode) + self.send_event(EventType.PRESET_EVENT, {"preset": self.preset_mode}) + if self.preset_mode not in HIDDEN_PRESETS: + self._attr_preset_mode = self.preset_mode + # Reset auto_start_stop switch delay to allow immediate restart when preset changes + if self.auto_start_stop_manager: + self.auto_start_stop_manager.reset_switch_delay() + + # Apply temperature + if self._state_manager.current_state.is_target_temperature_changed: + _LOGGER.info("%s - Applying new target temperature: %s", self, self.target_temperature) + self._attr_target_temperature = self.target_temperature + # Reset auto_start_stop switch delay to allow immediate restart when target temp changes + if self.auto_start_stop_manager: + self.auto_start_stop_manager.reset_switch_delay() + + # Apply hvac_mode + if self._state_manager.current_state.is_hvac_mode_changed: + _LOGGER.info("%s - Applying new hvac mode: %s", self, self.vtherm_hvac_mode) + # Delegate to all underlying + for under in self._underlyings: + sub_need_control_heating = await under.set_hvac_mode(self.vtherm_hvac_mode) or sub_need_control_heating + self._attr_hvac_mode = to_legacy_ha_hvac_mode(self.vtherm_hvac_mode) + self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": str(self.vtherm_hvac_mode)}) + # Remove eventual overpowering if we want to turn-off + if self.hvac_mode in [VThermHvacMode_OFF, VThermHvacMode_SLEEP] and self.power_manager.is_overpowering_detected: + await self.power_manager.set_overpowering(False) + + if changed: + self.recalculate(force=force) + self.reset_last_change_time_from_vtherm() + await self.async_control_heating(force=force or sub_need_control_heating) + + self.calculate_hvac_action() + self.update_custom_attributes() + self.async_write_ha_state() + else: + _LOGGER.debug("%s - current state did not change. Still %s", self, self._state_manager.current_state) + + self._state_manager.requested_state.reset_changed() + self._state_manager.current_state.reset_changed() + + return changed + + async def async_control_heating(self, timestamp=None, force=False) -> bool: + """The main function used to run the calculation at each cycle + Returns True if the cycle was successfully calculated, False otherwise.""" + + if timestamp: + # Timestamp is set only on periodical calls + write_event_log(_LOGGER, self, "Periodical control cycle started") + + _LOGGER.debug( + "%s - Checking new cycle. hvac_mode=%s, safety_state=%s, preset_mode=%s, force=%s", + self, + self.vtherm_hvac_mode, + self._safety_manager.safety_state, + self.vtherm_preset_mode, + force, + ) + + if not self.is_ready: + _LOGGER.info("%s - async_control_heating is called but the entity is not ready yet (not initialized or startup not done). Skip the cycle", self) + return False + + # check auto_window conditions + await self._window_manager.manage_window_auto(in_cycle=True) + + if timestamp and await self._safety_manager.refresh_and_update_if_changed(): + return False + + if self._safety_manager.is_safety_detected and self.is_over_climate: + _LOGGER.debug("%s - End of cycle (safety and over climate)", self) + + return True + + previous_hvac_action = self.hvac_action + # Call specific control heating + await self._control_heating_specific(timestamp, force) + + # Check for heating/cooling failures (only for TPI VTherms) + await self._heating_failure_detection_manager.refresh_state() + + # Check and repair state discrepancies + await self._repair_incorrect_state_manager.check_and_repair() + + self.calculate_hvac_action() + should_publish = True + algo_handler = getattr(self, "_algo_handler", None) + if algo_handler and hasattr(algo_handler, "should_publish_intermediate"): + should_publish = algo_handler.should_publish_intermediate() + if not should_publish and previous_hvac_action != self.hvac_action: + should_publish = True + + if should_publish: + self.update_custom_attributes() + self.async_write_ha_state() + + # For each manager display the manager state in debug and send an event with the manager state + current_state = self._state_manager.current_state.to_dict() + current_state["room_temperature"] = self.current_temperature + current_state["outdoor_temperature"] = self.current_outdoor_temperature + current_state["hvac_action"] = str(self.hvac_action) + current_state["power_percent"] = self.power_percent + manager_states = {} + for manager in self._managers: + try: + manager_states[manager.__class__.__name__] = manager.is_detected + except RuntimeError as e: + _LOGGER.error("%s - Error while getting is_detected state of manager %s: %s", self, manager.__class__.__name__, e) + + _LOGGER.debug("%s - End of cycle. current_state: %s, managers_states: %s", self, current_state, manager_states) + return True + + async def _control_heating_specific(self, timestamp=None, force=False): + """To be overridden by subclasses.""" + pass + + def reset_last_change_time_from_vtherm(self, old_preset_mode: VThermPreset | None = None): # pylint: disable=unused-argument + """Reset to now the last change time""" + self._last_change_time_from_vtherm = self.now + _LOGGER.debug( + "%s - last_change_time is now %s", self, self._last_change_time_from_vtherm + ) + + def reset_last_temperature_time(self, old_preset_mode: VThermPreset | str | None = None): + """Reset to now the last temperature time if conditions are satisfied""" + if self._state_manager.current_state.preset not in HIDDEN_PRESETS and old_preset_mode not in HIDDEN_PRESETS: + self._last_temperature_measure = self._last_ext_temperature_measure = ( + self.now + ) + + def find_preset_temp(self, preset_mode: VThermPreset): + """Find the right temperature of a preset considering + the presence if configured""" + if preset_mode is None or preset_mode == VThermPreset.NONE: + return self._attr_max_temp if self._ac_mode and self.vtherm_hvac_mode == VThermHvacMode_COOL else self._attr_min_temp + + if preset_mode == VThermPreset.SAFETY: + return self._state_manager.current_state.target_temperature + # In safety just keep the current target temperature, + # the thermostat should be off + + if preset_mode == VThermPreset.POWER: + return self._power_manager.power_temperature + if preset_mode == VThermPreset.ACTIVITY: + motion_preset = self._motion_manager.get_current_motion_preset() + if self._ac_mode and self.vtherm_hvac_mode == VThermHvacMode_COOL: + motion_preset = motion_preset + PRESET_AC_SUFFIX + + if motion_preset in self._presets: + if self._presence_manager.is_absence_detected: + return self._presets_away[motion_preset + PRESET_AWAY_SUFFIX] + else: + return self._presets[motion_preset] + else: + return None + else: + # Select _ac presets if in COOL Mode (or over_switch with _ac_mode) or OFF but requested is cool + preset_name = preset_mode + if ( + (self._ac_mode and self.vtherm_hvac_mode == VThermHvacMode_COOL) + or (self.vtherm_hvac_mode == VThermHvacMode_OFF and self.requested_state.hvac_mode == VThermHvacMode_COOL) + # issue #1958 - when window_action=fan_only temporarily switches the mode to FAN_ONLY, + # the user's intent (requested_state) is still COOL, so we must use AC presets. + or (self.vtherm_hvac_mode == VThermHvacMode_FAN_ONLY and self.requested_state.hvac_mode == VThermHvacMode_COOL) + # (self.is_over_switch and self._ac_mode) + # or self.vtherm_hvac_mode == VThermHvacMode_COOL + # or (self.vtherm_hvac_mode == VThermHvacMode_OFF and self.requested_state.hvac_mode == VThermHvacMode_COOL) + ): + # if self._ac_mode and self.vtherm_hvac_mode == VThermHvacMode_COOL: + preset_name += PRESET_AC_SUFFIX + + _LOGGER.info("%s - find preset temp: %s", self, preset_mode) + + temp_val = self._presets.get(preset_name, 0) + # if not self._presence_on or self._presence_state in [ + # None, + # STATE_ON, + # STATE_HOME, + # ]: + if self._presence_manager.is_absence_detected: + # We should return the preset_away temp val but if + # preset temp is 0, that means the user don't want to use + # the preset so we return 0, even if there is a value is preset_away + return self._presets_away.get(self.get_preset_away_name(preset_name), 0) if temp_val > 0 else temp_val + else: + return temp_val + + def get_preset_away_name(self, preset_mode: str) -> str: + """Get the preset name in away mode (when presence is off)""" + return preset_mode + PRESET_AWAY_SUFFIX + + def get_state_date_or_now(self, state: State) -> datetime: + """Extract the last_updated state from State or return now if not available""" + return state.last_updated.astimezone(self._current_tz) if isinstance(state.last_updated, datetime) else self.now + + def get_last_updated_date_or_now(self, state: State) -> datetime: + """Extract the last_updated state from State or return now if not available""" + return ( + state.last_updated.astimezone(self._current_tz) + if isinstance(state.last_updated, datetime) + else self.now + ) + + async def async_underlying_entity_turn_off(self): + """Turn heater toggleable device off. Used by Window, overpowering, + control_heating to turn all off""" + + if self._cycle_scheduler: + await self._cycle_scheduler.cancel_cycle() + + for under in self._underlyings: + await under.turn_off() + + def set_hvac_off_reason(self, hvac_off_reason: str | None): + """Set the reason of hvac_off""" + self._hvac_off_reason = hvac_off_reason + + def set_temperature_reason(self, temperature_reason: str | None): + """Set the reason of temperature""" + self._temperature_reason = temperature_reason + + async def check_central_mode(self, new_central_mode: str | None, old_central_mode: str | None): + """Take into account a central mode change""" + if not self.is_controlled_by_central_mode: + self._last_central_mode = None + return + + _LOGGER.info( + "%s - Central mode have change from %s to %s", + self, + old_central_mode, + new_central_mode, + ) + + self._last_central_mode = new_central_mode + + self.requested_state.force_changed() + await self.update_states() + return + + def recalculate(self, force=False): + """A utility function to force the calculation of a the algo and + update the custom attributes and write the state. + Should be overridden by super class + """ + raise NotImplementedError() + + def incremente_energy(self): + """increment the energy counter if device is active + Should be overridden by super class + """ + raise NotImplementedError() + + def calculate_hvac_action(self, _: list = None) -> HVACAction | None: + """Calculate the HVAC action based on the current state and underlying devices""" + if self.vtherm_hvac_mode == VThermHvacMode_OFF: + action = HVACAction.OFF + elif not self.is_device_active: + action = HVACAction.IDLE + elif self.vtherm_hvac_mode == VThermHvacMode_COOL: + action = HVACAction.COOLING + else: + action = HVACAction.HEATING + self._attr_hvac_action = action + + def update_custom_attributes(self): + """Update the custom extra attributes for the entity""" + + messages: list[str] = [] + if self.safety_manager.is_safety_detected: + messages.append(MSG_SAFETY_DETECTED) + if self.power_manager.is_overpowering_detected: + messages.append(MSG_OVERPOWERING_DETECTED) + if self.hvac_off_reason: + messages.append(self.hvac_off_reason) + if self.temperature_reason: + messages.append(self.temperature_reason) + + not_initialized_entities = [] + if not self.is_initialized: + messages.append(MSG_NOT_INITIALIZED) + # Find all underlying entities that are not initialized + for under in self._underlyings: + if not under.is_initialized: + not_initialized_entities.extend(under.state_manager.get_uninitialized_entities()) + + self._attr_extra_state_attributes: dict[str, Any] = { + "hvac_action": self.hvac_action, + "hvac_mode": self.hvac_mode, + "preset_mode": self.preset_mode, + "ema_temp": self._ema_temp, + "is_ready": self.is_ready, + "specific_states": { + "is_initialized": self.is_initialized, + "is_on": self.is_on, + "last_central_mode": self.last_central_mode, + "last_update_datetime": self.now.isoformat(), + "ext_current_temperature": self._cur_ext_temp, + "last_temperature_datetime": self._last_temperature_measure.astimezone(self._current_tz).isoformat(), + "last_ext_temperature_datetime": self._last_ext_temperature_measure.astimezone(self._current_tz).isoformat(), + "should_device_be_active": self.should_device_be_active, + "is_device_active": self.is_device_active, + "device_actives": self.device_actives, + "nb_device_actives": self.nb_device_actives, + "ema_temp": self._ema_temp, + "temperature_slope": round(self.last_temperature_slope or 0, 3), + "hvac_off_reason": self.hvac_off_reason, + ATTR_TOTAL_ENERGY: self.total_energy, + "last_change_time_from_vtherm": ( + self._last_change_time_from_vtherm.astimezone(self._current_tz).isoformat() if self._last_change_time_from_vtherm is not None else None + ), + "is_sleeping": self.is_sleeping, + "is_locked": self.lock_manager.is_locked, + "is_recalculate_scheduled": self.is_recalculate_scheduled, + "not_initialized_entities": not_initialized_entities, + "messages": messages, + }, + "configuration": { + "ac_mode": self._ac_mode, + "type": self.vtherm_type, + "proportional_function": self.proportional_function, + "is_controlled_by_central_mode": self.is_controlled_by_central_mode, + "target_temperature_step": self.target_temperature_step, + "timezone": str(self._current_tz), + "temperature_unit": self.temperature_unit, + "is_used_by_central_boiler": self.is_used_by_central_boiler, + "max_on_percent": self._max_on_percent, + "have_valve_regulation": self.have_valve_regulation, + "cycle_min": self._cycle_min, + }, + "preset_temperatures": { + "frost_temp": self._presets.get(VThermPreset.FROST, 0), + "eco_temp": self._presets.get(VThermPreset.ECO, 0), + "boost_temp": self._presets.get(VThermPreset.BOOST, 0), + "comfort_temp": self._presets.get(VThermPreset.COMFORT, 0), + "frost_away_temp": self._presets_away.get(self.get_preset_away_name(VThermPreset.FROST), 0), + "eco_away_temp": self._presets_away.get(self.get_preset_away_name(VThermPreset.ECO), 0), + "boost_away_temp": self._presets_away.get(self.get_preset_away_name(VThermPreset.BOOST), 0), + "comfort_away_temp": self._presets_away.get(self.get_preset_away_name(VThermPreset.COMFORT), 0), + }, + } + + self._state_manager.add_custom_attributes(self._attr_extra_state_attributes) + + for manager in self._managers: + manager.add_custom_attributes(self._attr_extra_state_attributes) + + def send_event(self, event_type: EventType, data: dict): + """Send an event""" + send_vtherm_event(self._hass, event_type=event_type, entity=self, data=data) + + async def async_turn_off(self) -> None: + await self.async_set_hvac_mode(VThermHvacMode_OFF) + + async def async_turn_on(self) -> None: + if self._ac_mode: + await self.async_set_hvac_mode(VThermHvacMode_COOL) + else: + await self.async_set_hvac_mode(VThermHvacMode_HEAT) + + def is_preset_configured(self, preset) -> bool: + """Returns True if the preset in argument is configured""" + return self._presets.get(preset, None) is not None + + ## + ## Device changes events + ## + + @callback + async def _async_temperature_changed(self, event: Event) -> callable: + """Handle temperature of the temperature sensor changes. + Return the function to dearm (clear) the window auto check""" + new_state: State = event.data.get("new_state") + write_event_log(_LOGGER, self, f"Temperature changed to state {new_state.state if new_state else None}") + + if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): + return + + dearm_window_auto = await self._async_update_temp(new_state) + self.recalculate() + + # Potentially it generates a safety event + if await self._safety_manager.refresh_and_update_if_changed() or (self.auto_start_stop_manager and await self.auto_start_stop_manager.refresh_and_update_if_changed()): + _LOGGER.info("%s - Change in safety alert or auto_start_stopis detected. Force update states", self) + return dearm_window_auto + + await self.async_control_heating(force=False) + + return dearm_window_auto + + @callback + async def _async_last_seen_temperature_changed(self, event: Event): + """Handle last seen temperature sensor changes.""" + new_state: State = event.data.get("new_state") + write_event_log(_LOGGER, self, f"Last seen temperature changed to state {new_state.state if new_state else None}") + + if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): + return + + # try to extract the datetime (from state) + try: + old_safety: bool = self._safety_manager.is_safety_detected + safety: bool = old_safety + + # Convert ISO 8601 string to datetime object + self._last_temperature_measure = self.get_last_updated_date_or_now(new_state) + # issue 690 - don't reset the last change time on lastSeen + # self.reset_last_change_time_from_vtherm() + _LOGGER.debug( + "%s - new last_temperature_measure is now: %s", + self, + self._last_temperature_measure, + ) + + # try to restart if we were in safety mode + if self._safety_manager.is_safety_detected: + safety = await self._safety_manager.refresh_state() + + if safety != old_safety: + _LOGGER.debug("%s - Change in safety alert is detected. Force update states", self) + self.requested_state.force_changed() + await self.update_states(force=True) + + except ValueError as err: + # La conversion a échoué, la chaîne n'est pas au format ISO 8601 + _LOGGER.warning( + "%s - impossible to convert last seen datetime %s. Error is: %s", + self, + new_state.state, + err, + ) + + async def _async_ext_temperature_changed(self, event: Event): + """Handle external temperature of the sensor changes.""" + new_state: State = event.data.get("new_state") + write_event_log(_LOGGER, self, f"Outdoor temperature changed to state {new_state.state if new_state else None}") + + if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): + return + + await self._async_update_ext_temp(new_state) + self.recalculate() + + # Potentially it generates a safety event + if await self._safety_manager.refresh_and_update_if_changed() or (self.auto_start_stop_manager and await self.auto_start_stop_manager.refresh_and_update_if_changed()): + _LOGGER.info("%s - Change in safety alert or auto_start_stopis detected. Force update states", self) + return + + await self.async_control_heating(force=False) + + @callback + async def _async_update_temp(self, state: State): + """Update thermostat with latest state from sensor.""" + try: + cur_temp = float(state.state) + if math.isnan(cur_temp) or math.isinf(cur_temp): + raise ValueError(f"Sensor has illegal state {state.state}") + self._cur_temp = cur_temp + + self._last_temperature_measure = self.get_state_date_or_now(state) + + # calculate the smooth_temperature with EMA calculation + self._ema_temp = self._ema_algo.calculate_ema(self._cur_temp, self._last_temperature_measure) + + _LOGGER.debug( + "%s - After setting _last_temperature_measure %s, state.last_changed.replace=%s", + self, + self._last_temperature_measure, + state.last_changed.astimezone(self._current_tz), + ) + + # try to restart if we were in safety mode + # if self._safety_manager.is_safety_detected: + # await self._safety_manager.refresh_state() + + # check window_auto + return await self._window_manager.manage_window_auto() + + except ValueError as ex: + _LOGGER.error("Unable to update temperature from sensor: %s", ex) + + @callback + async def _async_update_ext_temp(self, state: State): + """Update thermostat with latest state from sensor.""" + try: + cur_ext_temp = float(state.state) + if math.isnan(cur_ext_temp) or math.isinf(cur_ext_temp): + raise ValueError(f"Sensor has illegal state {state.state}") + self._cur_ext_temp = cur_ext_temp + self._last_ext_temperature_measure = self.get_state_date_or_now(state) + + _LOGGER.debug( + "%s - After setting _last_ext_temperature_measure %s, state.last_changed.replace=%s", + self, + self._last_ext_temperature_measure, + state.last_changed.astimezone(self._current_tz), + ) + + # try to restart if we were in safety mode + # if self._safety_manager.is_safety_detected: + # await self._safety_manager.refresh_state() + except ValueError as ex: + _LOGGER.error("Unable to update external temperature from sensor: %s", ex) + + ## + ## Services (actions) + ## + + async def service_set_presence(self, presence: str): + """Called by a service call: + service: versatile_thermostat.set_presence + data: + presence: "off" + target: + entity_id: climate.thermostat_1 + """ + if self.lock_manager.check_is_locked("service_set_presence"): + return + write_event_log(_LOGGER, self, f"Calling SERVICE_SET_PRESENCE, presence: {presence}") + await self._presence_manager.update_presence(presence) + await self.async_control_heating(force=True) + + async def service_set_safety( + self, + delay_min: int, + min_on_percent: float, + default_on_percent: float, + ): + """Called by a service call: + service: versatile_thermostat.set_safety + data: + delay_min: 15 + min_on_percent: 0.5 + default_on_percent: 0.2 + target: + entity_id: climate.thermostat_2 + """ + if self.lock_manager.check_is_locked("service_set_safety"): + return + write_event_log( + _LOGGER, self, f"Calling SERVICE_SET_SAFETY, delay_min: {delay_min}, " f"min_on_percent: {min_on_percent * 100} %, default_on_percent: {default_on_percent * 100} %" + ) + if delay_min: + self._safety_manager.set_safety_delay_min(delay_min) + if min_on_percent: + self._safety_manager.set_safety_min_on_percent(min_on_percent) + if default_on_percent: + self._safety_manager.set_safety_default_on_percent(default_on_percent) + + await self.async_control_heating() + self.update_custom_attributes() + self.async_write_ha_state() + + async def service_set_window_bypass_state(self, window_bypass: bool): + """Called by a service call: + service: versatile_thermostat.set_window_bypass + data: + window_bypass: True + target: + entity_id: climate.thermostat_1 + """ + if self.lock_manager.check_is_locked("service_set_window_bypass_state"): + return + write_event_log(_LOGGER, self, f"Calling SERVICE_SET_WINDOW_BYPASS, window_bypass: {window_bypass}") + if await self._window_manager.set_window_bypass(window_bypass): + self.requested_state.force_changed() + await self.update_states(force=True) + + async def service_set_hvac_mode_sleep(self): + """Set the hvac_mode to SLEEP mode (valid only for over_climate with valve regulation): + service: versatile_thermostat.set_hvac_mode_sleep + target: + entity_id: climate.thermostat_1 + """ + if self.lock_manager.check_is_locked("service_set_hvac_mode_sleep"): + return + write_event_log(_LOGGER, self, "Calling SERVICE_SET_HVAC_MODE_SLEEP") + raise NotImplementedError("service_set_hva_mode_sleep not implemented for this kind of thermostat. Only for over_climate with valve regulation is supported") + + async def service_lock(self, code: str | None = None): + """Handle the lock service call.""" + + if not self.lock_manager.has_lock_settings_enabled: + _LOGGER.error("%s - Cannot lock thermostat: no lock settings enabled (neither users nor automations)", self) + raise HomeAssistantError("Cannot lock thermostat: no lock settings enabled. At least one of 'lock users' or 'lock automations' must be enabled.") + + if self.lock_manager.change_lock_state(True, code): + self.update_custom_attributes() + self.async_write_ha_state() + + async def service_unlock(self, code: str | None = None): + """Handle the unlock service call.""" + if self.lock_manager.change_lock_state(False, code): + self.update_custom_attributes() + self.async_write_ha_state() + + async def service_set_tpi_parameters( + self, + tpi_coef_int: float | None = None, + tpi_coef_ext: float | None = None, + minimal_activation_delay: int | None = None, + minimal_deactivation_delay: int | None = None, + tpi_threshold_low: float | None = None, + tpi_threshold_high: float | None = None, + ): + """Stub method for TPI parameter service on non-TPI thermostats. + + This service is only available for switch/valve type thermostats that use TPI algorithm. + For over_climate thermostats, this service is not supported. + + Raises: + ServiceValidationError: Always raised to indicate the service is not available + """ + raise ServiceValidationError( + f"{self} - The set_tpi_parameters service is only available for switch/valve type thermostats. " + "This thermostat does not use TPI algorithm." + ) + + async def service_set_auto_tpi_mode(self, auto_tpi_mode: bool): + """Stub method for Auto TPI mode service on non-TPI thermostats. + + This service is only available for switch/valve type thermostats that use TPI algorithm. + For over_climate thermostats, this service is not supported. + + Raises: + ServiceValidationError: Always raised to indicate the service is not available + """ + raise ServiceValidationError( + f"{self} - The set_auto_tpi_mode service is only available for switch/valve type thermostats. " + "This thermostat does not use TPI algorithm." + ) + + async def service_auto_tpi_calibrate_capacity( + self, + save_to_config: bool, + min_power_threshold: int, + start_date: datetime | None = None, + end_date: datetime | None = None, + ): + """Stub method for Auto TPI capacity calibration service on non-TPI thermostats. + + This service is only available for switch/valve type thermostats that use TPI algorithm. + For over_climate thermostats, this service is not supported. + + Raises: + ServiceValidationError: Always raised to indicate the service is not available + """ + raise ServiceValidationError( + f"{self} - The auto_tpi_calibrate_capacity service is only available for switch/valve type thermostats. " + "This thermostat does not use TPI algorithm." + ) + + async def service_set_timed_preset(self, preset: str, duration_minutes: float): + """Called by a service call: + service: versatile_thermostat.set_timed_preset + data: + preset: "boost" + duration_minutes: 30 + target: + entity_id: climate.thermostat_1 + + Force a preset for a given duration. After the duration expires, + the original preset (from requested_state) will be restored. + """ + if self.lock_manager.check_is_locked("service_set_timed_preset"): + return + + write_event_log( + _LOGGER, + self, + f"Calling SERVICE_SET_TIMED_PRESET, preset: {preset}, duration: {duration_minutes} min", + ) + + # Validate that the preset is in the list of accepted presets + preset_enum = VThermPreset(preset) if preset else None + if preset_enum not in self._vtherm_preset_modes: + raise ServiceValidationError( + f"{self} - The preset '{preset}' is not available for this thermostat. " f"Available presets are: {[str(p) for p in self._vtherm_preset_modes]}" + ) + + # Validate duration + if duration_minutes <= 0: + raise ServiceValidationError(f"{self} - The duration must be a positive number, got {duration_minutes}") + + # Set the timed preset + success = await self._timed_preset_manager.set_timed_preset(preset_enum, duration_minutes) + if not success: + raise ServiceValidationError(f"{self} - Failed to set timed preset '{preset}' for {duration_minutes} minutes") + + async def service_cancel_timed_preset(self): + """Called by a service call: + service: versatile_thermostat.cancel_timed_preset + target: + entity_id: climate.thermostat_1 + + Cancel any active timed preset and restore the original preset. + """ + if self.lock_manager.check_is_locked("service_cancel_timed_preset"): + return + + write_event_log(_LOGGER, self, "Calling SERVICE_CANCEL_TIMED_PRESET") + + await self._timed_preset_manager.cancel_timed_preset() + + async def service_recalibrate_valves(self, delay_seconds: int): + """Stub method for recalibrate_valves service on unsupported thermostat types. + + Raises: + ServiceValidationError: Always raised to indicate the service is not available + """ + raise ServiceValidationError( + f"{self} - The recalibrate_valves service is only available for ThermostatClimateValve thermostats." + ) + + async def service_download_logs( + self, + log_level: str = "DEBUG", + period_start: datetime | None = None, + period_end: datetime | None = None, + ): + """Called by the download_logs entity service.""" + from vtherm_api.log_collector import async_export_logs # pylint: disable=import-outside-toplevel + + handler = self._hass.data.get(DOMAIN, {}).get("log_handler") + if handler is None: + _LOGGER.warning("%s - Log collector is not initialized", self._name) + return + + await async_export_logs( + hass=self._hass, + handler=handler, + thermostat_name=self._name, + entity_id=self.entity_id, + log_level=log_level, + period_start=period_start, + period_end=period_end, + config_entry=self._entry_infos, + ) + + ## + ## For testing purpose + ## + # @deprecated + def _set_now(self, now: datetime): + """Set the now timestamp. This is only for tests purpose + This method should be replaced by the vthermAPI equivalent""" + VersatileThermostatAPI.get_vtherm_api(self._hass)._set_now(now) # pylint: disable=protected-access + + # @deprecated + @property + def now(self) -> datetime: + """Get now. The local datetime or the overloaded _set_now date + This method should be replaced by the vthermAPI equivalent""" + return VersatileThermostatAPI.get_vtherm_api(self._hass).now diff --git a/custom_components/versatile_thermostat/binary_sensor.py b/custom_components/versatile_thermostat/binary_sensor.py new file mode 100644 index 0000000..f3096dc --- /dev/null +++ b/custom_components/versatile_thermostat/binary_sensor.py @@ -0,0 +1,477 @@ +""" Implements the VersatileThermostat binary sensors component """ +# pylint: disable=unused-argument, line-too-long + +import logging +from vtherm_api.log_collector import get_vtherm_logger + +from homeassistant.core import ( + HomeAssistant, + callback, + Event, + # CoreState, +) + +from homeassistant.const import STATE_ON, STATE_OFF + +from homeassistant.helpers.device_registry import DeviceInfo + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorDeviceClass, +) +from homeassistant.config_entries import ConfigEntry + +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .vtherm_central_api import VersatileThermostatAPI +from .base_entity import VersatileThermostatBaseEntity +from .const import ( + DOMAIN, + DEVICE_MANUFACTURER, + CONF_NAME, + CONF_USE_POWER_FEATURE, + CONF_USE_PRESENCE_FEATURE, + CONF_USE_MOTION_FEATURE, + CONF_USE_WINDOW_FEATURE, + CONF_USE_HEATING_FAILURE_DETECTION_FEATURE, + CONF_THERMOSTAT_TYPE, + CONF_THERMOSTAT_CENTRAL_CONFIG, + CONF_USE_CENTRAL_BOILER_FEATURE, + overrides, + EventType, +) + +_LOGGER = get_vtherm_logger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the VersatileThermostat binary sensors with config flow.""" + unique_id = entry.entry_id + name = entry.data.get(CONF_NAME) + _LOGGER.debug("%s - Calling async_setup_entry entry=%s, data=%s", name, entry.entry_id, entry.data) + + vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) + + entities = None + + if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG: + if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE): + # we capture here the configuration for central boiler feature + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + api.central_boiler_manager.post_init(entry.data) + entities = [ + CentralBoilerBinarySensor(hass, unique_id, name, entry.data), + ] + else: + entities = [ + SafetyBinarySensor(hass, unique_id, name, entry.data), + WindowByPassBinarySensor(hass, unique_id, name, entry.data), + ] + if entry.data.get(CONF_USE_MOTION_FEATURE): + entities.append(MotionBinarySensor(hass, unique_id, name, entry.data)) + if entry.data.get(CONF_USE_WINDOW_FEATURE): + entities.append(WindowBinarySensor(hass, unique_id, name, entry.data)) + if entry.data.get(CONF_USE_PRESENCE_FEATURE): + entities.append(PresenceBinarySensor(hass, unique_id, name, entry.data)) + if entry.data.get(CONF_USE_POWER_FEATURE): + entities.append(OverpoweringBinarySensor(hass, unique_id, name, entry.data)) + if entry.data.get(CONF_USE_HEATING_FAILURE_DETECTION_FEATURE): + entities.append(HeatingFailureBinarySensor(hass, unique_id, name, entry.data)) + + if entities: + async_add_entities(entities, True) + + +class SafetyBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): + """Representation of a BinarySensor which exposes the security state""" + + _attr_has_entity_name = True + _attr_translation_key = "safety_state" + + def __init__( + self, + hass: HomeAssistant, + unique_id, + name, # pylint: disable=unused-argument + entry_infos, + ) -> None: + """Initialize the SafetyState Binary sensor""" + super().__init__(hass, unique_id, name) + self._attr_unique_id = f"{self._device_name}_safety_state" + self._attr_is_on = False + + @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) + + old_state = self._attr_is_on + self._attr_is_on = self.my_climate.safety_manager.is_safety_detected + if old_state != self._attr_is_on: + self.async_write_ha_state() + return + + @property + def device_class(self) -> BinarySensorDeviceClass | None: + return BinarySensorDeviceClass.SAFETY + + @property + def icon(self) -> str | None: + if self._attr_is_on: + return "mdi:shield-alert" + else: + return "mdi:shield-check-outline" + + +class HeatingFailureBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): + """Representation of a BinarySensor which exposes the heating failure state""" + + _attr_has_entity_name = True + _attr_translation_key = "heating_failure_state" + + def __init__( + self, + hass: HomeAssistant, + unique_id, + name, # pylint: disable=unused-argument + entry_infos, + ) -> None: + """Initialize the HeatingFailure Binary sensor""" + super().__init__(hass, unique_id, name) + self._attr_unique_id = f"{self._device_name}_heating_failure_state" + self._attr_is_on = False + + @callback + async def async_my_climate_changed(self, event: Event = None): + """Called when my climate have change""" + + old_state = self._attr_is_on + self._attr_is_on = self.my_climate.heating_failure_detection_manager.is_failure_detected + if old_state != self._attr_is_on: + self.async_write_ha_state() + return + + @property + def device_class(self) -> BinarySensorDeviceClass | None: + return BinarySensorDeviceClass.PROBLEM + + @property + def icon(self) -> str | None: + if self._attr_is_on: + return "mdi:radiator-off" + else: + return "mdi:radiator" + + +class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): + """Representation of a BinarySensor which exposes the overpowering state""" + + _attr_has_entity_name = True + _attr_translation_key = "overpowering_state" + + def __init__( + self, + hass: HomeAssistant, + unique_id, + name, # pylint: disable=unused-argument + entry_infos, + ) -> None: + """Initialize the OverpoweringState Binary sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_unique_id = f"{self._device_name}_overpowering_state" + self._attr_is_on = False + + @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) + + old_state = self._attr_is_on + self._attr_is_on = self.my_climate.overpowering_state is STATE_ON + if old_state != self._attr_is_on: + self.async_write_ha_state() + return + + @property + def device_class(self) -> BinarySensorDeviceClass | None: + return BinarySensorDeviceClass.POWER + + @property + def icon(self) -> str | None: + if self._attr_is_on: + return "mdi:flash-alert-outline" + else: + return "mdi:flash-outline" + + +class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): + """Representation of a BinarySensor which exposes the window state""" + + _attr_has_entity_name = True + _attr_translation_key = "window_state" + + def __init__( + self, + hass: HomeAssistant, + unique_id, + name, # pylint: disable=unused-argument + entry_infos, + ) -> None: + """Initialize the WindowState Binary sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_unique_id = f"{self._device_name}_window_state" + self._attr_is_on = False + + @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) + + old_state = self._attr_is_on + # Issue 120 - only take defined presence value + if self.my_climate.window_state in [ + STATE_ON, + STATE_OFF, + ] or self.my_climate.window_auto_state in [STATE_ON, STATE_OFF]: + 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 + + @property + def device_class(self) -> BinarySensorDeviceClass | None: + return BinarySensorDeviceClass.WINDOW + + @property + def icon(self) -> str | None: + if self._attr_is_on: + if self.my_climate.window_state == STATE_ON: + return "mdi:window-open-variant" + else: + return "mdi:window-open" + else: + return "mdi:window-closed-variant" + + +class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): + """Representation of a BinarySensor which exposes the motion state""" + + _attr_has_entity_name = True + _attr_translation_key = "motion_state" + + def __init__( + self, + hass: HomeAssistant, + unique_id, + name, # pylint: disable=unused-argument + entry_infos, + ) -> None: + """Initialize the MotionState Binary sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_unique_id = f"{self._device_name}_motion_state" + self._attr_is_on = False + + @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) + old_state = self._attr_is_on + # Issue 120 - only take defined presence value + if self.my_climate.motion_state in [STATE_ON, STATE_OFF]: + self._attr_is_on = self.my_climate.motion_state == STATE_ON + if old_state != self._attr_is_on: + self.async_write_ha_state() + return + + @property + def device_class(self) -> BinarySensorDeviceClass | None: + return BinarySensorDeviceClass.MOTION + + @property + def icon(self) -> str | None: + if self._attr_is_on: + return "mdi:motion-sensor" + else: + return "mdi:motion-sensor-off" + + +class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): + """Representation of a BinarySensor which exposes the presence state""" + + _attr_has_entity_name = True + _attr_translation_key = "presence_state" + + def __init__( + self, + hass: HomeAssistant, + unique_id, + name, # pylint: disable=unused-argument + entry_infos, + ) -> None: + """Initialize the PresenceState Binary sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_unique_id = f"{self._device_name}_presence_state" + self._attr_is_on = False + + @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) + old_state = self._attr_is_on + # Issue 120 - only take defined presence value + if self.my_climate.presence_state in [STATE_ON, STATE_OFF]: + self._attr_is_on = self.my_climate.presence_state == STATE_ON + if old_state != self._attr_is_on: + self.async_write_ha_state() + return + + @property + def device_class(self) -> BinarySensorDeviceClass | None: + return BinarySensorDeviceClass.PRESENCE + + @property + def icon(self) -> str | None: + if self._attr_is_on: + return "mdi:home-account" + else: + return "mdi:nature-people" + + +class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): + """Representation of a BinarySensor which exposes the Window ByPass state""" + + _attr_has_entity_name = True + _attr_translation_key = "window_bypass_state" + + def __init__( + self, + hass: HomeAssistant, + unique_id, + name, # pylint: disable=unused-argument + entry_infos, + ) -> None: + """Initialize the WindowByPass Binary sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_unique_id = f"{self._device_name}_window_bypass_state" + self._attr_is_on = False + + @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) + old_state = self._attr_is_on + if self.my_climate.is_window_bypass in [True, False]: + self._attr_is_on = self.my_climate.is_window_bypass + if old_state != self._attr_is_on: + self.async_write_ha_state() + return + + @property + def device_class(self) -> BinarySensorDeviceClass | None: + return BinarySensorDeviceClass.RUNNING + + @property + def icon(self) -> str | None: + if self._attr_is_on: + return "mdi:window-shutter-cog" + else: + return "mdi:window-shutter-auto" + + +class CentralBoilerBinarySensor(BinarySensorEntity): + """Representation of a BinarySensor which exposes the Central Boiler state""" + + _attr_has_entity_name = True + _attr_translation_key = "central_boiler_state" + + _entity_component_unrecorded_attributes = BinarySensorEntity._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access + frozenset({"is_central_boiler_configured", "is_central_boiler_ready", "central_boiler_manager"}) + ) + + def __init__( + self, + hass: HomeAssistant, + unique_id, + name, # pylint: disable=unused-argument + entry_infos, + ) -> None: + """Initialize the CentralBoiler Binary sensor""" + self._config_id = unique_id + self._attr_unique_id = "central_boiler_state" + self._attr_is_on = False + self._device_name = entry_infos.get(CONF_NAME) + self._entry_infos = entry_infos + self._hass = hass + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + entry_type=None, + identifiers={(DOMAIN, self._config_id)}, + name=self._device_name, + manufacturer=DEVICE_MANUFACTURER, + model=DOMAIN, + ) + + @property + def device_class(self) -> BinarySensorDeviceClass | None: + return BinarySensorDeviceClass.RUNNING + + @property + def icon(self) -> str | None: + if self._attr_is_on: + return "mdi:water-boiler" + else: + return "mdi:water-boiler-off" + + @overrides + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass) + api.central_boiler_manager.register_central_boiler(self) + + # Listen to central boiler events + self.async_on_remove( + self._hass.bus.async_listen( + EventType.CENTRAL_BOILER_EVENT.value, + self._handle_central_boiler_event, + ) + ) + + self.update_custom_attributes() + self.async_write_ha_state() + + @callback + def _handle_central_boiler_event(self, event): + """Handle central boiler event to update internal state.""" + _LOGGER.debug("%s - Received central boiler event: %s", self, event.data) + if "central_boiler" in event.data: + new_state = event.data["central_boiler"] + if self._attr_is_on != new_state: + self._attr_is_on = new_state + self.refresh_custom_attributes() + + def refresh_custom_attributes(self): + """Refresh the custom attributes""" + self.update_custom_attributes() + self.async_write_ha_state() + + def update_custom_attributes(self): + """Update the custom extra attributes for the entity""" + self._attr_extra_state_attributes = {"central_boiler_state": STATE_ON if self._attr_is_on else STATE_OFF} + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api() + cb_manager = api.central_boiler_manager + cb_manager.add_custom_attributes(self._attr_extra_state_attributes) + + def __str__(self): + return f"VersatileThermostat-{self.name}" diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py new file mode 100644 index 0000000..a650fba --- /dev/null +++ b/custom_components/versatile_thermostat/climate.py @@ -0,0 +1,231 @@ +""" Implements the VersatileThermostat climate component """ + +import logging +from vtherm_api.log_collector import get_vtherm_logger + + +import voluptuous as vol + +from homeassistant.core import HomeAssistant, SupportsResponse + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers import selector +from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import async_setup_reload_service + +from homeassistant.helpers import entity_platform + +from homeassistant.const import ( + CONF_NAME, + STATE_ON, + STATE_OFF, + STATE_HOME, + STATE_NOT_HOME, +) + +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 +from .vtherm_central_api import VersatileThermostatAPI + +_LOGGER = get_vtherm_logger(__name__) + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the VersatileThermostat thermostat with config flow.""" + unique_id = entry.entry_id + name = entry.data.get(CONF_NAME) + _LOGGER.debug("%s - Calling async_setup_entry entry=%s, data=%s", name, entry.entry_id, entry.data) + + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + 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: + # Initialize the central power manager + vtherm_api = VersatileThermostatAPI.get_vtherm_api(hass) + vtherm_api.reset_central_config() + vtherm_api.central_power_manager.post_init(entry.data) + return + + # Instantiate the right base class + entity = None + if vt_type == CONF_THERMOSTAT_SWITCH: + entity = ThermostatOverSwitch(hass, unique_id, name, entry.data) + elif vt_type == CONF_THERMOSTAT_CLIMATE: + 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: + _LOGGER.error( + "Cannot create Versatile Thermostat name=%s of type %s which is unknown", + name, + vt_type, + ) + return + + async_add_entities([entity], True) + + # Add services + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_SET_PRESENCE, + { + vol.Required("presence"): vol.In( + [STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME] + ), + }, + "service_set_presence", + ) + + platform.async_register_entity_service( + SERVICE_SET_SAFETY, + { + vol.Optional("delay_min"): cv.positive_int, + vol.Optional("min_on_percent"): vol.Coerce(float), + vol.Optional("default_on_percent"): vol.Coerce(float), + }, + "service_set_safety", + ) + + platform.async_register_entity_service( + SERVICE_SET_WINDOW_BYPASS, + { + vol.Required("window_bypass"): vol.In([True, False]), + }, + "service_set_window_bypass_state", + ) + + platform.async_register_entity_service( + SERVICE_SET_AUTO_REGULATION_MODE, + { + vol.Required("auto_regulation_mode"): vol.In( + ["None", "Light", "Medium", "Strong", "Slow", "Expert"] + ), + }, + "service_set_auto_regulation_mode", + ) + + platform.async_register_entity_service( + SERVICE_SET_AUTO_FAN_MODE, + { + vol.Required("auto_fan_mode"): vol.In( + ["None", "Low", "Medium", "High", "Turbo"] + ), + }, + "service_set_auto_fan_mode", + ) + + platform.async_register_entity_service( + SERVICE_SET_HVAC_MODE_SLEEP, + {}, + "service_set_hvac_mode_sleep", + ) + + platform.async_register_entity_service( + SERVICE_LOCK, + { + vol.Optional("code"): cv.string, + }, + "service_lock", + ) + + platform.async_register_entity_service( + SERVICE_UNLOCK, + { + vol.Optional("code"): cv.string, + }, + "service_unlock", + ) + + platform.async_register_entity_service( + SERVICE_SET_TPI_PARAMETERS, + { + vol.Optional(CONF_TPI_COEF_INT): selector.NumberSelector(selector.NumberSelectorConfig(min=0.0, max=10.0, step=0.01, mode=selector.NumberSelectorMode.BOX)), + vol.Optional(CONF_TPI_COEF_EXT): selector.NumberSelector(selector.NumberSelectorConfig(min=0.0, max=1.0, step=0.001, mode=selector.NumberSelectorMode.BOX)), + vol.Optional(CONF_MINIMAL_ACTIVATION_DELAY): cv.positive_int, + vol.Optional(CONF_MINIMAL_DEACTIVATION_DELAY): cv.positive_int, + vol.Optional(CONF_TPI_THRESHOLD_LOW): selector.NumberSelector(selector.NumberSelectorConfig(min=-10.0, max=10.0, step=0.1, mode=selector.NumberSelectorMode.BOX)), + vol.Optional(CONF_TPI_THRESHOLD_HIGH): selector.NumberSelector(selector.NumberSelectorConfig(min=-10.0, max=10.0, step=0.1, mode=selector.NumberSelectorMode.BOX)), + }, + "service_set_tpi_parameters", + ) + + platform.async_register_entity_service( + SERVICE_SET_AUTO_TPI_MODE, + { + vol.Required("auto_tpi_mode"): vol.In([True, False]), + vol.Optional("reinitialise", default=True): vol.In([True, False]), + vol.Optional("allow_kint_boost_on_stagnation", default=False): vol.In( + [True, False] + ), + vol.Optional( + "allow_kext_compensation_on_overshoot", default=False + ): vol.In([True, False]), + }, + "service_set_auto_tpi_mode", + ) + + platform.async_register_entity_service( + SERVICE_AUTO_TPI_CALIBRATE_CAPACITY, + { + vol.Optional("start_date"): selector.DateTimeSelector(), + vol.Optional("end_date"): selector.DateTimeSelector(), + vol.Optional("save_to_config", default=False): vol.In([True, False]), + vol.Optional("min_power_threshold", default=95): selector.NumberSelector( + selector.NumberSelectorConfig(min=80, max=100, step=1, mode=selector.NumberSelectorMode.SLIDER) + ), + }, + "service_auto_tpi_calibrate_capacity", + supports_response=SupportsResponse.OPTIONAL, + ) + + platform.async_register_entity_service( + SERVICE_SET_TIMED_PRESET, + { + vol.Required("preset"): cv.string, + vol.Required("duration_minutes"): selector.NumberSelector( + selector.NumberSelectorConfig(min=1, max=1440, step=1, mode=selector.NumberSelectorMode.BOX) + ), + }, + "service_set_timed_preset", + ) + + platform.async_register_entity_service( + SERVICE_CANCEL_TIMED_PRESET, + {}, + "service_cancel_timed_preset", + ) + + platform.async_register_entity_service( + SERVICE_RECALIBRATE_VALVES, + { + vol.Required("delay_seconds"): vol.All(vol.Coerce(int), vol.Range(min=0, max=300)), + }, + "service_recalibrate_valves", + supports_response=SupportsResponse.OPTIONAL, + ) + + platform.async_register_entity_service( + SERVICE_DOWNLOAD_LOGS, + { + vol.Optional("log_level", default="DEBUG"): vol.In( + ["DEBUG", "INFO", "WARNING", "ERROR"] + ), + vol.Optional("period_start"): selector.DateTimeSelector(), + vol.Optional("period_end"): selector.DateTimeSelector(), + }, + "service_download_logs", + ) diff --git a/custom_components/versatile_thermostat/commons.py b/custom_components/versatile_thermostat/commons.py new file mode 100644 index 0000000..d68ef42 --- /dev/null +++ b/custom_components/versatile_thermostat/commons.py @@ -0,0 +1,196 @@ +""" Some usefull commons class """ + +# pylint: disable=line-too-long + +import logging +from vtherm_api.log_collector import get_vtherm_logger +import warnings + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers import entity_registry as er + +from .const import ServiceConfigurationError, DOMAIN + +_LOGGER = get_vtherm_logger(__name__) + + +def round_to_nearest(n: float, x: float) -> float: + """Round a number to the nearest x (which should be decimal but not null) + Example: + nombre1 = 3.2 + nombre2 = 4.7 + x = 0.3 + + nombre_arrondi1 = round_to_nearest(nombre1, x) + nombre_arrondi2 = round_to_nearest(nombre2, x) + + print(nombre_arrondi1) # Output: 3.3 + print(nombre_arrondi2) # Output: 4.6 + """ + assert x > 0 + return round(n * (1 / x)) / (1 / x) + + +def check_and_extract_service_configuration(service_config) -> dict: + """Raise a ServiceConfigurationError. In return you have a dict formatted like follows. + Example if you call with 'climate.central_boiler/climate.set_temperature/temperature:10': + { + "service_domain": "climate", + "service_name": "set_temperature", + "entity_id": "climate.central_boiler", + "entity_domain": "climate", + "entity_name": "central_boiler", + "data": { + "temperature": "10" + }, + "attribute_name": "temperature", + "attribute_value: "10" + } + + For this example 'switch.central_boiler/switch.turn_off' you will have this: + { + "service_domain": "switch", + "service_name": "turn_off", + "entity_id": "switch.central_boiler", + "entity_domain": "switch", + "entity_name": "central_boiler", + "data": { }, + } + + All values are striped (white space are removed) and are string + """ + + ret = {} + + if service_config is None: + return ret + + parties = service_config.split("/") + if len(parties) < 2: + raise ServiceConfigurationError( + f"Incorrect service configuration. Service {service_config} should be formatted with: 'entity_name/service_name[/data]'. See README for more information." + ) + entity_id = parties[0] + service_name = parties[1] + + service_infos = service_name.split(".") + if len(service_infos) != 2: + raise ServiceConfigurationError( + f"Incorrect service configuration. The service {service_config} should be formatted like: 'domain.service_name' (ex: 'switch.turn_on'). See README for more information." + ) + + ret.update( + { + "service_domain": service_infos[0].strip(), + "service_name": service_infos[1].strip(), + } + ) + + entity_infos = entity_id.split(".") + if len(entity_infos) != 2: + raise ServiceConfigurationError( + f"Incorrect service configuration. The entity_id {entity_id} should be formatted like: 'domain.entity_name' (ex: 'switch.central_boiler_switch'). See README for more information." + ) + + ret.update( + { + "entity_domain": entity_infos[0].strip(), + "entity_name": entity_infos[1].strip(), + "entity_id": entity_id.strip(), + } + ) + + if len(parties) == 3: + data = parties[2] + if len(data) > 0: + data_infos = None + data_infos = data.split(":") + if ( + len(data_infos) != 2 + or len(data_infos[0]) <= 0 + or len(data_infos[1]) <= 0 + ): + raise ServiceConfigurationError( + f"Incorrect service configuration. The data {data} should be formatted like: 'attribute:value' (ex: 'value:25'). See README for more information." + ) + + ret.update( + { + "attribute_name": data_infos[0].strip(), + "attribute_value": data_infos[1].strip(), + "data": {data_infos[0].strip(): data_infos[1].strip()}, + } + ) + else: + raise ServiceConfigurationError( + f"Incorrect service configuration. The data {data} should be formatted like: 'attribute:value' (ex: 'value:25'). See README for more information." + ) + else: + ret.update({"data": {}}) + + _LOGGER.debug( + "check_and_extract_service_configuration(%s) gives '%s'", service_config, ret + ) + return ret + + +def deprecated(message): + """A decorator to indicate that the method/attribut is deprecated""" + + def decorator(func): + def wrapper(*args, **kwargs): + warnings.warn( + f"{func.__name__} is deprecated: {message}", + DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator + + +def write_event_log(logger: logging.Logger, vtherm: "BaseThermostat", message: str): + """Write an event log entry for the thermostat.""" + logger.info("%s - ---------------------> NEW EVENT: %s --------------------------------------------------------------", vtherm, message) + + +async def cleanup_orphan_entity( + hass: HomeAssistant, + entry: ConfigEntry, + domain: str, + device_name: str, + unique_id_suffix: str, +) -> None: + """Remove an orphan entity from entity registry if it exists but is no longer needed. + + This generic function can be used for any entity type that needs to be + conditionally created/removed based on configuration changes. + + Args: + hass: The Home Assistant instance. + entry: The config entry for the thermostat. + domain: The entity domain (e.g., "sensor", "binary_sensor", "switch"). + device_name: The device name used to build the unique_id. + unique_id_suffix: The suffix appended to the device name (e.g., "auto_tpi_learning"). + """ + registry = er.async_get(hass) + # Build the expected unique_id for the entity + expected_unique_id = f"{device_name}_{unique_id_suffix}" + + # Find entity by unique_id within this config entry + entity_id = registry.async_get_entity_id( + domain, DOMAIN, expected_unique_id + ) + + if entity_id: + entity_entry = registry.async_get(entity_id) + if entity_entry and entity_entry.config_entry_id == entry.entry_id: + _LOGGER.debug( + "Removing orphan %s entity %s from registry (feature disabled)", + domain, + entity_id + ) + registry.async_remove(entity_id) diff --git a/custom_components/versatile_thermostat/commons_type.py b/custom_components/versatile_thermostat/commons_type.py new file mode 100644 index 0000000..79ed1ad --- /dev/null +++ b/custom_components/versatile_thermostat/commons_type.py @@ -0,0 +1,2 @@ +# import logging +from vtherm_api.commons_type import ConfigData diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py new file mode 100644 index 0000000..6facb35 --- /dev/null +++ b/custom_components/versatile_thermostat/config_flow.py @@ -0,0 +1,1321 @@ +# pylint: disable=line-too-long, too-many-lines, invalid-name + +"""Config flow for Versatile Thermostat integration.""" +from __future__ import annotations + +from typing import Any +import re +from vtherm_api.log_collector import get_vtherm_logger +import copy +from collections.abc import Mapping # pylint: disable=import-error +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow as HAConfigFlow, + OptionsFlow, +) +from homeassistant.const import UnitOfTemperature + +from homeassistant.data_entry_flow import FlowHandler, FlowResult + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import +from .vtherm_central_api import VersatileThermostatAPI +from .commons import check_and_extract_service_configuration + +COMES_FROM = "comes_from" +PLUGINS_LINK_PLACEHOLDER = "plugins_link" +PLUGINS_LINK = "[plugins](https://www.versatile-thermostat.org/en/plugins/)" + +_LOGGER = get_vtherm_logger(__name__) + +def add_suggested_values_to_schema( + data_schema: vol.Schema, suggested_values: Mapping[str, Any] +) -> vol.Schema: + """Make a copy of the schema, populated with suggested values. + + For each schema marker matching items in `suggested_values`, + the `suggested_value` will be set. The existing `suggested_value` will + be left untouched if there is no matching item. + """ + schema = {} + for key, val in data_schema.schema.items(): + new_key = key + if key in suggested_values and isinstance(key, vol.Marker): + # Copy the marker to not modify the flow schema + new_key = copy.copy(key) + new_key.description = {"suggested_value": suggested_values[key]} + schema[new_key] = val + _LOGGER.debug("add_suggested_values_to_schema: schema=%s", schema) + return vol.Schema(schema) + + +class VersatileThermostatBaseConfigFlow(FlowHandler): + """The base Config flow class. Used to put some code in commons.""" + + VERSION = CONFIG_VERSION + MINOR_VERSION = CONFIG_MINOR_VERSION + + def __init__(self, infos) -> None: + super().__init__() + _LOGGER.debug("CTOR BaseConfigFlow infos: %s", infos) + self._infos: dict = infos + self._learning_type_changed = False + self._placeholders = { + CONF_NAME: "", + PLUGINS_LINK_PLACEHOLDER: PLUGINS_LINK, + } + + # VTherm API should have been initialized before arriving here + vtherm_api = VersatileThermostatAPI.get_vtherm_api() + if vtherm_api is not None: + self._central_config = vtherm_api.find_central_configuration() + else: + self._central_config = None + + self._init_central_config_flags(infos) + self._init_feature_flags(infos) + + def _init_feature_flags(self, _): + """Fix features selection depending to infos""" + is_central_config = ( + self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG + ) + + self._infos[CONF_USE_WINDOW_FEATURE] = ( + self._infos.get(CONF_USE_WINDOW_CENTRAL_CONFIG, False) + 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] = self._infos.get( + 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, False) + or self._infos.get(CONF_USE_POWER_FEATURE, False) + or (is_central_config and 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, False) + or self._infos.get(CONF_PRESENCE_SENSOR) is not None + ) + + self._infos[CONF_USE_CENTRAL_BOILER_FEATURE] = ( + self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV) is not None + 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 + ) + + self._infos[CONF_USE_HEATING_FAILURE_DETECTION_FEATURE] = self._infos.get( + CONF_USE_HEATING_FAILURE_DETECTION_FEATURE, False + ) + + def _init_central_config_flags(self, infos): + """Initialisation of central configuration flags""" + is_empty: bool = not bool(infos) + for config in ( + CONF_USE_MAIN_CENTRAL_CONFIG, + CONF_USE_TPI_CENTRAL_CONFIG, + CONF_USE_WINDOW_CENTRAL_CONFIG, + CONF_USE_MOTION_CENTRAL_CONFIG, + CONF_USE_POWER_CENTRAL_CONFIG, + CONF_USE_PRESETS_CENTRAL_CONFIG, + CONF_USE_PRESENCE_CENTRAL_CONFIG, + CONF_USE_ADVANCED_CENTRAL_CONFIG, + CONF_USE_LOCK_CENTRAL_CONFIG, + CONF_USE_CENTRAL_MODE, + ): + if not is_empty: + current_config = self._infos.get(config, 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] + + 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 is_sync_device_internal_temp_selected(self, infos) -> bool: + """True if the synchronize device internal temperature mode is selected""" + return infos.get(CONF_SYNC_DEVICE_INTERNAL_TEMP, False) is True + + 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_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_closing and nb_closing > 0): + ret = False + return ret + + def check_sync_device_internal_temp_nb_entities(self, data: dict, step_id=None) -> bool: + """Check the number of entities for synchronize device internal temperature""" + if step_id not in ["type", "sync_device_internal_temp", "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 == "sync_device_internal_temp" else self._infos + + ret = True + if self.is_sync_device_internal_temp_selected(underlyings_to_check) and step_id != "type": + nb_unders = len(underlyings_to_check.get(CONF_UNDERLYING_LIST)) + nb_sync = len(regulation_infos_to_check.get(CONF_SYNC_ENTITY_LIST, [])) + if nb_unders != nb_sync: + 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. + """ + + # check the entity_ids + for conf in [ + CONF_UNDERLYING_LIST, + CONF_TEMP_SENSOR, + CONF_EXTERNAL_TEMP_SENSOR, + CONF_WINDOW_SENSOR, + CONF_MOTION_SENSOR, + CONF_POWER_SENSOR, + CONF_MAX_POWER_SENSOR, + CONF_PRESENCE_SENSOR, + CONF_OFFSET_CALIBRATION_LIST, + CONF_OPENING_DEGREE_LIST, + CONF_CLOSING_DEGREE_LIST, + ]: + d = data.get(conf, None) # pylint: disable=invalid-name + if not isinstance(d, list): + 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 + 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_AUTO_OPEN_THRESHOLD) + + # Check that is USE_CENTRAL config is used, that a central config exists + if self._central_config is None: + for conf in [ + CONF_USE_MAIN_CENTRAL_CONFIG, + CONF_USE_TPI_CENTRAL_CONFIG, + CONF_USE_WINDOW_CENTRAL_CONFIG, + CONF_USE_MOTION_CENTRAL_CONFIG, + CONF_USE_POWER_CENTRAL_CONFIG, + 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( + "The use of central configuration need a central configuration Versatile Thermostat instance" + ) + raise NoCentralConfig(conf) + + # Check the service for central boiler format + if self._infos.get(CONF_USE_CENTRAL_BOILER_FEATURE): + for conf in [ + CONF_CENTRAL_BOILER_ACTIVATION_SRV, + CONF_CENTRAL_BOILER_DEACTIVATION_SRV, + ]: + try: + check_and_extract_service_configuration(data.get(conf)) + 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() + + if not self.check_sync_device_internal_temp_nb_entities(data, step_id): + raise SyncDeviceInternalTempNbEntitiesIncorrect() + + # Check that the min_opening_degrees is correctly set + raw_list = data.get(CONF_MIN_OPENING_DEGREES, None) + min_opening_degrees_list = [] + if raw_list: + try: + # Validation : Convertir la liste saisie + min_opening_degrees_list = [int(x.strip()) for x in raw_list.split(",")] + + # Optionnel : Vérifiez des conditions supplémentaires sur la liste + if any(x < 0 for x in min_opening_degrees_list): + raise ValueError + except ValueError as exc: + raise ValveRegulationMinOpeningDegreesIncorrect( + CONF_MIN_OPENING_DEGREES + ) from exc + + # Check that the max_opening_degrees is correctly set + raw_list = data.get(CONF_MAX_OPENING_DEGREES, None) + max_opening_degrees_list = [] + if raw_list: + try: + # Validation : Convertir la liste saisie + max_opening_degrees_list = [int(x.strip()) for x in raw_list.split(",")] + + # Check that max opening degrees are <= the underlying valve max opening and > 0 + valves_entities = data.get(CONF_OPENING_DEGREE_LIST, []) + for valve_idx, valve_max in enumerate(max_opening_degrees_list): + if valve_max <= 0 or valve_max > self.hass.states.get(valves_entities[valve_idx]).attributes.get("max", 100): + raise ValueError + except ValueError as exc: + raise ValveRegulationMaxOpeningDegreesIncorrect( + CONF_MAX_OPENING_DEGREES + ) from exc + + # Check that max_opening_degrees > min_opening_degrees for each underlying + # If both lists exist, check that max > min for each index + if min_opening_degrees_list and max_opening_degrees_list: + # Get the number of underlyings to know how many values to check + nb_underlyings = len(self._infos.get(CONF_UNDERLYING_LIST, [])) + for idx in range(nb_underlyings): + min_val = min_opening_degrees_list[idx] if idx < len(min_opening_degrees_list) else 0 + max_val = max_opening_degrees_list[idx] if idx < len(max_opening_degrees_list) else 100 + + if max_val <= min_val: + raise ValveRegulationMinMaxOpeningDegreesIncorrect( + CONF_MAX_OPENING_DEGREES + ) + + # Check the VSWITCH configuration. There should be the same number of vswitch_on (resp. vswitch_off) than the number of underlying entity + if self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_SWITCH and step_id == "type": + if not self.check_vswitch_configuration(data): + raise VirtualSwitchConfigurationIncorrect(CONF_VSWITCH_ON_CMD_LIST) + + # Check the lock code format + if data.get(CONF_LOCK_CODE) is not None: + if not re.match(r"^\d{4}$", str(data.get(CONF_LOCK_CODE))): + raise LockCodeIncorrect() + + def check_vswitch_configuration(self, data) -> bool: + """Check the Virtual switch configuration and return True if the configuration is correct""" + nb_under = len(data.get(CONF_UNDERLYING_LIST, [])) + + # check format of each command_on + for command in data.get(CONF_VSWITCH_ON_CMD_LIST, []) + data.get(CONF_VSWITCH_OFF_CMD_LIST, []): + pattern = r"^(?P[^\s/]+)(?:/(?P[^\s:]+)(?::(?P[^\s]+))?)?$" + if not re.match(pattern, command): + return False + + nb_command_on = len(data.get(CONF_VSWITCH_ON_CMD_LIST, [])) + nb_command_off = len(data.get(CONF_VSWITCH_OFF_CMD_LIST, [])) + if (nb_command_on == nb_under or nb_command_on == 0) and (nb_command_off == nb_under or nb_command_off == 0): + # There is enough command_on and off + # Check if one under is not a switch (which have default command). + if any( + not thermostat_type.startswith(SWITCH_DOMAIN) and not thermostat_type.startswith(INPUT_BOOLEAN_DOMAIN) for thermostat_type in data.get(CONF_UNDERLYING_LIST, []) + ): + if nb_command_on != nb_under or nb_command_off != nb_under: + return False + return True + return False + + def check_config_complete(self, infos) -> bool: + """True if the config is now complete (ie all mandatory attributes are set)""" + is_central_config = ( + infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG + ) + if is_central_config: + if ( + infos.get(CONF_NAME) is None + or infos.get(CONF_EXTERNAL_TEMP_SENSOR) is None + ): + return False + + if infos.get(CONF_USE_POWER_FEATURE, False) is True and ( + infos.get(CONF_POWER_SENSOR, None) is None + or infos.get(CONF_MAX_POWER_SENSOR, None) is None + ): + return False + + if ( + infos.get(CONF_USE_PRESENCE_FEATURE, False) is True + and infos.get(CONF_PRESENCE_SENSOR, None) is None + ): + return False + + if self._infos[CONF_USE_CENTRAL_BOILER_FEATURE] and ( + not self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV, False) + or len(self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV)) <= 0 + or not self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV, False) + or len(self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV)) <= 0 + ): + return False + else: + if ( + infos.get(CONF_NAME) is None + or infos.get(CONF_TEMP_SENSOR) is None + or infos.get(CONF_CYCLE_MIN) is None + ): + return False + + if ( + infos.get(CONF_USE_MAIN_CENTRAL_CONFIG, False) is False + and infos.get(CONF_EXTERNAL_TEMP_SENSOR) is None + ): + return False + + # checks that at least one underlying is set but not it central configuration + if len(infos.get(CONF_UNDERLYING_LIST, [])) < 1: + return False + + if ( + infos.get(CONF_USE_MOTION_FEATURE, False) is True + and infos.get(CONF_MOTION_SENSOR, None) is None + ): + return False + + if infos.get(CONF_USE_POWER_FEATURE, False) is True and infos.get(CONF_USE_POWER_CENTRAL_CONFIG, False) is False and infos.get(CONF_PRESET_POWER, None) is None: + return False + + if ( + infos.get(CONF_USE_PRESENCE_FEATURE, False) is True + and infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False) is False + and infos.get(CONF_PRESENCE_SENSOR, None) is None + ): + return False + + if infos.get(CONF_USE_ADVANCED_CENTRAL_CONFIG, False) is False and ( + infos.get(CONF_SAFETY_DELAY_MIN, -1) == -1 or infos.get(CONF_SAFETY_MIN_ON_PERCENT, -1) == -1 or infos.get(CONF_SAFETY_DEFAULT_ON_PERCENT, -1) == -1 + ): + 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 + + if not self.check_sync_device_internal_temp_nb_entities(infos, "check_complete"): + return False + + return True + + 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) + for key, _ in data_schema.schema.items(): + if key not in user_input and isinstance(key, vol.Marker): + _LOGGER.debug( + "add_empty_values_to_user_input: %s is not in user_input", key + ) + if key in self._infos: + self._infos.pop(key) + # else: This don't work but I don't know why. _infos seems broken after this (Not serializable exactly) + # self._infos[key] = user_input[key] + + _LOGGER.debug("merge_user_input: infos is now %s", self._infos) + + async def generic_step(self, step_id, data_schema, user_input, next_step_function): + """A generic method step""" + _LOGGER.debug( + "Into ConfigFlow.async_step_%s user_input=%s", step_id, user_input + ) + + defaults = self._infos.copy() + errors = {} + + if user_input is not None: + defaults.update(user_input or {}) + try: + await self.validate_input(user_input, step_id) + except UnknownEntity as err: + errors[str(err)] = "unknown_entity" + except WindowOpenDetectionMethod as err: + errors[str(err)] = "window_open_detection_method" + except NoCentralConfig as err: + errors[str(err)] = "no_central_config" + except ServiceConfigurationError as err: + 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 SyncDeviceInternalTempNbEntitiesIncorrect as err: + errors["base"] = "sync_device_internal_temp_nb_entities_incorrect" + except ValveRegulationMinOpeningDegreesIncorrect as err: + errors[str(err)] = "min_opening_degrees_format" + except ValveRegulationMaxOpeningDegreesIncorrect as err: + errors[str(err)] = "max_opening_degrees_format" + except ValveRegulationMinMaxOpeningDegreesIncorrect as err: + errors[str(err)] = "min_max_opening_degrees_inconsistent" + except VirtualSwitchConfigurationIncorrect as err: + errors["base"] = "vswitch_configuration_incorrect" + except LockCodeIncorrect: + errors["base"] = "lock_code_incorrect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self.merge_user_input(data_schema, user_input) + # Add default values for central config flags + self._init_central_config_flags(self._infos) + _LOGGER.debug("_info is now: %s", self._infos) + return await next_step_function() + + # ds = schema_defaults(data_schema, **defaults) # pylint: disable=invalid-name + ds = add_suggested_values_to_schema( + data_schema=data_schema, suggested_values=defaults + ) # pylint: disable=invalid-name + + return self.async_show_form( + step_id=step_id, + data_schema=ds, + errors=errors, + description_placeholders=self._placeholders, + ) + + async def async_step_user(self, user_input: dict | None = None) -> FlowResult: + """Handle the flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_user user_input=%s", user_input) + + # Always do a fresh lookup (not from __init__ cache) to handle same-session deletions + vtherm_api = VersatileThermostatAPI.get_vtherm_api() + central_config = vtherm_api.find_central_configuration() if vtherm_api else None + + # Exclude the central config type if one already exists (it must be unique) + if central_config is not None: + available_types = [t for t in CONF_THERMOSTAT_TYPES if t != CONF_THERMOSTAT_CENTRAL_CONFIG] + schema = vol.Schema( + { + vol.Required(CONF_THERMOSTAT_TYPE, default=CONF_THERMOSTAT_SWITCH): selector.SelectSelector( + selector.SelectSelectorConfig( + options=available_types, + translation_key="thermostat_type", + mode="list", + ) + ) + } + ) + else: + schema = STEP_USER_DATA_SCHEMA + + return await self.generic_step("user", schema, user_input, self.async_step_menu) + + async def async_step_configuration_not_complete( + self, user_input: dict | None = None + ) -> FlowResult: + """A fake step to handle the incomplete configuration flow""" + return await self.async_step_menu(user_input) + + async def async_step_menu(self, user_input: dict | None = None) -> FlowResult: + """Handle the flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_menu user_input=%s", user_input) + + is_central_config = self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG + + menu_options = ["main", "features"] + if not is_central_config: + menu_options.append("type") + + 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") + + if self._infos.get(CONF_THERMOSTAT_TYPE) in [ + CONF_THERMOSTAT_SWITCH, + CONF_THERMOSTAT_VALVE, + CONF_THERMOSTAT_CLIMATE, + ]: + menu_options.append("presets") + + if is_central_config and self._infos.get(CONF_USE_CENTRAL_BOILER_FEATURE, False) is True: + menu_options.append("central_boiler") + + if self._infos.get(CONF_USE_WINDOW_FEATURE, False) is True: + menu_options.append("window") + + if self._infos.get(CONF_USE_MOTION_FEATURE, False) is True: + menu_options.append("motion") + + if self._infos.get(CONF_USE_POWER_FEATURE, False) is True: + menu_options.append("power") + + if self._infos.get(CONF_USE_PRESENCE_FEATURE, False) is True: + menu_options.append("presence") + + if ( + self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False) is True + and self._infos[CONF_THERMOSTAT_TYPE] + in [ + CONF_THERMOSTAT_CLIMATE, + ] + and not self.is_valve_regulation_selected(self._infos) + ): + menu_options.append("auto_start_stop") + + if self.is_valve_regulation_selected(self._infos): + menu_options.append("valve_regulation") + + if self._infos.get(CONF_SYNC_DEVICE_INTERNAL_TEMP, False) is True: + menu_options.append("sync_device_internal_temp") + + menu_options.append("advanced") + menu_options.append("lock") + + # Add heating failure detection menu if feature is enabled + if self._infos.get(CONF_USE_HEATING_FAILURE_DETECTION_FEATURE, False) is True: + menu_options.append("heating_failure_detection") + + if self.check_config_complete(self._infos): + menu_options.append("finalize") + else: + _LOGGER.info("The configuration is not terminated") + menu_options.append("configuration_not_complete") + + return self.async_show_menu( + step_id="menu", + menu_options=menu_options, + description_placeholders=self._placeholders, + ) + + async def async_step_main(self, user_input: dict | None = None) -> FlowResult: + """Handle the flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_main user_input=%s", user_input) + + next_step = self.async_step_menu + if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: + self._infos[CONF_NAME] = CENTRAL_CONFIG_NAME + schema = STEP_CENTRAL_MAIN_DATA_SCHEMA + else: + schema = STEP_MAIN_DATA_SCHEMA + + if ( + user_input + and user_input.get(CONF_USE_MAIN_CENTRAL_CONFIG, False) is False + ): + if user_input and self._infos.get(COMES_FROM) == "async_step_spec_main": + schema = STEP_CENTRAL_MAIN_DATA_SCHEMA + del self._infos[COMES_FROM] + else: + next_step = self.async_step_spec_main + + # Add default values for temperature if not set + if self._infos.get(CONF_TEMP_MIN) is None: + # We use the system default unit + unit = self.hass.config.units.temperature_unit + if unit == UnitOfTemperature.FAHRENHEIT: + self._infos[CONF_TEMP_MIN] = 45 + self._infos[CONF_TEMP_MAX] = 95 + self._infos[CONF_STEP_TEMPERATURE] = 1.0 + else: + self._infos[CONF_TEMP_MIN] = 7 + self._infos[CONF_TEMP_MAX] = 35 + self._infos[CONF_STEP_TEMPERATURE] = 0.1 + + return await self.generic_step("main", schema, user_input, next_step) + + async def async_step_spec_main(self, user_input: dict | None = None) -> FlowResult: + """Handle the specific main flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_spec_main user_input=%s", user_input) + + if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: + schema = STEP_CENTRAL_MAIN_DATA_SCHEMA + else: + schema = STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA + next_step = self.async_step_menu + + self._infos[COMES_FROM] = "async_step_spec_main" + + # This will return to async_step_main (to keep the "main" step) + return await self.generic_step("main", schema, user_input, next_step) + + async def async_step_central_boiler( + self, user_input: dict | None = None + ) -> FlowResult: + """Handle the central boiler flow steps""" + _LOGGER.debug( + "Into ConfigFlow.async_step_central_boiler user_input=%s", user_input + ) + + schema = STEP_CENTRAL_BOILER_SCHEMA + next_step = self.async_step_menu + + return await self.generic_step("central_boiler", schema, user_input, next_step) + + async def async_step_type(self, user_input: dict | None = None) -> FlowResult: + """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", build_step_thermostat_switch_schema(), user_input, self.async_step_menu + ) + elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_VALVE: + return await self.generic_step( + "type", build_step_thermostat_valve_schema(), user_input, self.async_step_menu + ) + else: + return await self.generic_step( + "type", + STEP_THERMOSTAT_CLIMATE, + user_input, + self.async_step_menu, + ) + + async def async_step_features(self, user_input: dict | None = None) -> FlowResult: + """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 and not self.is_valve_regulation_selected(self._infos): + schema = STEP_CLIMATE_FEATURES_DATA_SCHEMA + elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CLIMATE and self.is_valve_regulation_selected(self._infos): + schema = STEP_CLIMATE_VALVE_FEATURES_DATA_SCHEMA + + return await self.generic_step( + "features", + 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 = build_step_valve_regulation_schema() + 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_sync_device_internal_temp(self, user_input: dict | None = None) -> FlowResult: + """Handle the synchronize device internal temperature configuration step""" + _LOGGER.debug("Into ConfigFlow.async_step_sync_device_internal_temp user_input=%s", user_input) + + schema = STEP_SYNC_DEVICE_INTERNAL_TEMP + self._infos[COMES_FROM] = None + next_step = self.async_step_menu + + return await self.generic_step("sync_device_internal_temp", 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) + + next_step = self.async_step_menu + if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: + schema = STEP_CENTRAL_TPI_DATA_SCHEMA_CENTRAL + else: + schema = STEP_TPI_DATA_SCHEMA + + if user_input: + if user_input.get(CONF_USE_TPI_CENTRAL_CONFIG, False) is False: + if self._infos.get(COMES_FROM) == "async_step_spec_tpi": + schema = STEP_CENTRAL_TPI_DATA_SCHEMA + del self._infos[COMES_FROM] + if user_input.get(CONF_AUTO_TPI_MODE) is True: + next_step = self.async_step_auto_tpi_configuration + else: + next_step = self.async_step_spec_tpi + elif user_input.get(CONF_AUTO_TPI_MODE) is True: + next_step = self.async_step_auto_tpi_configuration + + return await self.generic_step("tpi", schema, user_input, next_step) + + async def async_step_spec_tpi(self, user_input: dict | None = None) -> FlowResult: + """Handle the specific TPI flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_spec_tpi user_input=%s", user_input) + + schema = STEP_CENTRAL_TPI_DATA_SCHEMA + self._infos[COMES_FROM] = "async_step_spec_tpi" + next_step = self.async_step_menu + + if user_input and user_input.get(CONF_AUTO_TPI_MODE) is True: + next_step = self.async_step_auto_tpi_configuration + + return await self.generic_step("tpi", schema, user_input, next_step) + + async def async_step_auto_tpi_configuration(self, user_input: dict | None = None) -> FlowResult: + """Handle the Auto TPI configuration step""" + _LOGGER.debug( + "Into ConfigFlow.async_step_auto_tpi_configuration user_input=%s", user_input + ) + schema = STEP_AUTO_TPI_CONFIGURATION_SCHEMA + + # Logic for next step + next_step = self._step_auto_tpi_after_configuration + + # Update placeholders with unit + self._placeholders["temperature_unit"] = self.hass.config.units.temperature_unit + self._placeholders["unit"] = self.hass.config.units.temperature_unit + + if user_input is not None: + new_type = user_input.get(CONF_AUTO_TPI_LEARNING_TYPE) + old_type = self._infos.get(CONF_AUTO_TPI_LEARNING_TYPE) + if new_type != old_type: + self._learning_type_changed = True + + return await self.generic_step("auto_tpi_configuration", schema, user_input, next_step) + + async def _step_auto_tpi_after_configuration(self, user_input: dict | None = None) -> FlowResult: + """Logic after step 1""" + # Apply defaults if learning type changed or method not set + if getattr(self, "_learning_type_changed", False) or self._infos.get(CONF_AUTO_TPI_CALCULATION_METHOD) is None: + self._apply_learning_type_defaults() + self._learning_type_changed = False + + if self._infos.get(CONF_AUTO_TPI_ENABLE_ADVANCED_SETTINGS, False): + # Direct jump to method step + if self._infos.get(CONF_AUTO_TPI_CALCULATION_METHOD) == AUTO_TPI_METHOD_EMA: + return await self.async_step_auto_tpi_ema_settings() + else: + return await self.async_step_auto_tpi_avg_settings() + else: + return await self.async_step_menu() + + def _apply_learning_type_defaults(self): + """Apply defaults based on learning type""" + learning_type = self._infos.get(CONF_AUTO_TPI_LEARNING_TYPE) + + if learning_type == AUTO_TPI_LEARNING_TYPE_DISCOVERY: + # Discovery -> Weighted Average, Weight 1 + self._infos[CONF_AUTO_TPI_CALCULATION_METHOD] = AUTO_TPI_METHOD_AVG + self._infos[CONF_AUTO_TPI_AVG_INITIAL_WEIGHT] = 1 + elif learning_type == AUTO_TPI_LEARNING_TYPE_FINE_TUNING: + # Fine Tuning -> EMA, Alpha 0.08, Decay 0.12 + self._infos[CONF_AUTO_TPI_CALCULATION_METHOD] = AUTO_TPI_METHOD_EMA + self._infos[CONF_AUTO_TPI_EMA_ALPHA] = 0.08 + self._infos[CONF_AUTO_TPI_EMA_DECAY_RATE] = 0.12 + + async def async_step_auto_tpi_avg_settings( + self, user_input: dict | None = None + ) -> FlowResult: + """Handle the Auto TPI average settings step""" + _LOGGER.debug( + "Into ConfigFlow.async_step_auto_tpi_avg_settings user_input=%s", user_input + ) + schema = STEP_AUTO_TPI_AVG_SETTINGS_SCHEMA + next_step = self.async_step_menu + + # Update placeholders with unit + self._placeholders["temperature_unit"] = self.hass.config.units.temperature_unit + self._placeholders["unit"] = self.hass.config.units.temperature_unit + + return await self.generic_step("auto_tpi_avg_settings", schema, user_input, next_step) + + async def async_step_auto_tpi_ema_settings( + self, user_input: dict | None = None + ) -> FlowResult: + """Handle the Auto TPI EMA settings step""" + _LOGGER.debug( + "Into ConfigFlow.async_step_auto_tpi_ema_settings user_input=%s", user_input + ) + schema = STEP_AUTO_TPI_EMA_SETTINGS_SCHEMA + next_step = self.async_step_menu + return await self.generic_step("auto_tpi_ema_settings", schema, user_input, next_step) + + async def async_step_presets(self, user_input: dict | None = None) -> FlowResult: + """Handle the presets flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_presets user_input=%s", user_input) + + next_step = self.async_step_menu # advanced + schema = STEP_PRESETS_DATA_SCHEMA + + # In Central config -> display the next step immedialty + if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: + # Call directly the next step, we have nothing to display here + return await self.async_step_window() # = self.async_step_window + + return await self.generic_step("presets", schema, user_input, next_step) + + async def async_step_window(self, user_input: dict | None = None) -> FlowResult: + """Handle the window sensor flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_window user_input=%s", user_input) + + next_step = self.async_step_menu + if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: + schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA + else: + schema = STEP_WINDOW_DATA_SCHEMA + + if ( + user_input + and user_input.get(CONF_USE_WINDOW_CENTRAL_CONFIG, False) is False + ): + if ( + user_input + and self._infos.get(COMES_FROM) == "async_step_spec_window" + ): + if self._infos.get(CONF_WINDOW_SENSOR) is not None: + schema = STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA + else: + schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA + del self._infos[COMES_FROM] + else: + next_step = self.async_step_spec_window + + return await self.generic_step("window", schema, user_input, next_step) + + async def async_step_spec_window( + self, user_input: dict | None = None + ) -> FlowResult: + """Handle the specific window flow steps""" + _LOGGER.debug( + "Into ConfigFlow.async_step_spec_window user_input=%s", user_input + ) + + schema = STEP_CENTRAL_WINDOW_DATA_SCHEMA + if self._infos.get(CONF_WINDOW_SENSOR) is not None: + schema = STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA + + self._infos[COMES_FROM] = "async_step_spec_window" + + next_step = self.async_step_motion + + # This will return to async_step_main (to keep the "main" step) + return await self.generic_step("window", schema, user_input, next_step) + + async def async_step_motion(self, user_input: dict | None = None) -> FlowResult: + """Handle the window and motion sensor flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_motion user_input=%s", user_input) + + next_step = self.async_step_menu + if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: + schema = STEP_CENTRAL_MOTION_DATA_SCHEMA + else: + schema = STEP_MOTION_DATA_SCHEMA + + if ( + user_input + and user_input.get(CONF_USE_MOTION_CENTRAL_CONFIG, False) is False + ): + if ( + user_input + and self._infos.get(COMES_FROM) == "async_step_spec_motion" + ): + schema = STEP_CENTRAL_MOTION_DATA_SCHEMA + del self._infos[COMES_FROM] + else: + next_step = self.async_step_spec_motion + + return await self.generic_step("motion", schema, user_input, next_step) + + async def async_step_spec_motion( + self, user_input: dict | None = None + ) -> FlowResult: + """Handle the specific motion flow steps""" + _LOGGER.debug( + "Into ConfigFlow.async_step_spec_motion user_input=%s", user_input + ) + + schema = STEP_CENTRAL_MOTION_DATA_SCHEMA + + self._infos[COMES_FROM] = "async_step_spec_motion" + + next_step = self.async_step_menu + + # This will return to async_step_main (to keep the "main" step) + return await self.generic_step("motion", schema, user_input, next_step) + + async def async_step_power(self, user_input: dict | None = None) -> FlowResult: + """Handle the power management flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_power user_input=%s", user_input) + + next_step = self.async_step_menu + if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: + schema = STEP_CENTRAL_POWER_DATA_SCHEMA + else: + schema = STEP_POWER_DATA_SCHEMA + + if ( + user_input + and user_input.get(CONF_USE_POWER_CENTRAL_CONFIG, False) is False + ): + if ( + user_input + and self._infos.get(COMES_FROM) == "async_step_spec_power" + ): + schema = STEP_CENTRAL_POWER_DATA_SCHEMA + del self._infos[COMES_FROM] + else: + next_step = self.async_step_spec_power + + return await self.generic_step("power", schema, user_input, next_step) + + async def async_step_spec_power(self, user_input: dict | None = None) -> FlowResult: + """Handle the specific power flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_spec_power user_input=%s", user_input) + + schema = STEP_NON_CENTRAL_POWER_DATA_SCHEMA + + self._infos[COMES_FROM] = "async_step_spec_power" + + next_step = self.async_step_menu + + # This will return to async_step_power (to keep the "power" step) + return await self.generic_step("power", schema, user_input, next_step) + + async def async_step_presence(self, user_input: dict | None = None) -> FlowResult: + """Handle the presence management flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input) + + next_step = self.async_step_menu + if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: + schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA + else: + schema = STEP_PRESENCE_DATA_SCHEMA + + if ( + user_input + and user_input.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False) is False + ): + if ( + user_input + and self._infos.get(COMES_FROM) == "async_step_spec_presence" + ): + schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA + del self._infos[COMES_FROM] + else: + next_step = self.async_step_spec_presence + + return await self.generic_step("presence", schema, user_input, next_step) + + async def async_step_spec_presence( + self, user_input: dict | None = None + ) -> FlowResult: + """Handle the specific power flow steps""" + _LOGGER.debug( + "Into ConfigFlow.async_step_spec_presence user_input=%s", user_input + ) + + schema = STEP_CENTRAL_PRESENCE_DATA_SCHEMA + + self._infos[COMES_FROM] = "async_step_spec_presence" + + next_step = self.async_step_menu + + # This will return to async_step_power (to keep the "power" step) + return await self.generic_step("presence", schema, user_input, next_step) + + async def async_step_advanced(self, user_input: dict | None = None) -> FlowResult: + """Handle the advanced parameter flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_advanced user_input=%s", user_input) + + next_step = self.async_step_menu + if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: + schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA + else: + schema = STEP_ADVANCED_DATA_SCHEMA + + if ( + user_input + and user_input.get(CONF_USE_ADVANCED_CENTRAL_CONFIG, False) is False + ): + if ( + user_input + and self._infos.get(COMES_FROM) == "async_step_spec_advanced" + ): + schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA + del self._infos[COMES_FROM] + else: + next_step = self.async_step_spec_advanced + + return await self.generic_step("advanced", schema, user_input, next_step) + + async def async_step_spec_advanced( + self, user_input: dict | None = None + ) -> FlowResult: + """Handle the specific advanced flow steps""" + _LOGGER.debug( + "Into ConfigFlow.async_step_spec_advanced user_input=%s", user_input + ) + + schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA + + self._infos[COMES_FROM] = "async_step_spec_advanced" + + next_step = self.async_step_advanced + + # This will return to async_step_presence (to keep the "presence" step) + return await self.generic_step("advanced", schema, user_input, next_step) + + async def async_step_lock(self, user_input: dict | None = None) -> FlowResult: + """Handle the lock flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_lock user_input=%s", user_input) + + next_step = self.async_step_menu + if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: + schema = STEP_CENTRAL_LOCK_DATA_SCHEMA + else: + schema = STEP_LOCK_DATA_SCHEMA + + if ( + user_input + and user_input.get(CONF_USE_LOCK_CENTRAL_CONFIG, False) is False + ): + if ( + user_input + and self._infos.get(COMES_FROM) == "async_step_spec_lock" + ): + schema = STEP_CENTRAL_LOCK_DATA_SCHEMA + del self._infos[COMES_FROM] + else: + next_step = self.async_step_spec_lock + + return await self.generic_step("lock", schema, user_input, next_step) + + async def async_step_spec_lock( + self, user_input: dict | None = None + ) -> FlowResult: + """Handle the specific lock flow steps""" + _LOGGER.debug( + "Into ConfigFlow.async_step_spec_lock user_input=%s", user_input + ) + + schema = STEP_CENTRAL_LOCK_DATA_SCHEMA + + self._infos[COMES_FROM] = "async_step_spec_lock" + + next_step = self.async_step_menu + + return await self.generic_step("lock", schema, user_input, next_step) + + async def async_step_heating_failure_detection(self, user_input: dict | None = None) -> FlowResult: + """Handle the heating failure detection flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_heating_failure_detection user_input=%s", user_input) + + next_step = self.async_step_menu + if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: + schema = STEP_CENTRAL_HEATING_FAILURE_DETECTION_SCHEMA + else: + schema = STEP_HEATING_FAILURE_DETECTION_SCHEMA + + if user_input and user_input.get(CONF_USE_HEATING_FAILURE_DETECTION_CENTRAL_CONFIG, False) is False: + if user_input and self._infos.get(COMES_FROM) == "async_step_spec_heating_failure_detection": + schema = STEP_CENTRAL_HEATING_FAILURE_DETECTION_SCHEMA + del self._infos[COMES_FROM] + else: + next_step = self.async_step_spec_heating_failure_detection + + return await self.generic_step("heating_failure_detection", schema, user_input, next_step) + + async def async_step_spec_heating_failure_detection(self, user_input: dict | None = None) -> FlowResult: + """Handle the specific heating failure detection flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_spec_heating_failure_detection user_input=%s", user_input) + + # here we reuse the central schema because it is the same for non central config + schema = STEP_CENTRAL_HEATING_FAILURE_DETECTION_SCHEMA + + self._infos[COMES_FROM] = "async_step_spec_heating_failure_detection" + + next_step = self.async_step_heating_failure_detection + + return await self.generic_step("heating_failure_detection", schema, user_input, next_step) + + async def async_step_finalize(self, _): + """Finalize the creation. Should be overriden by underlyings""" + if not self._infos[CONF_USE_WINDOW_FEATURE]: + self._infos[CONF_USE_WINDOW_CENTRAL_CONFIG] = False + if CONF_WINDOW_SENSOR in self._infos: + del self._infos[CONF_WINDOW_SENSOR] + if CONF_WINDOW_AUTO_CLOSE_THRESHOLD in self._infos: + del self._infos[CONF_WINDOW_AUTO_CLOSE_THRESHOLD] + if CONF_WINDOW_AUTO_OPEN_THRESHOLD in self._infos: + del self._infos[CONF_WINDOW_AUTO_OPEN_THRESHOLD] + if CONF_WINDOW_AUTO_MAX_DURATION in self._infos: + del self._infos[CONF_WINDOW_AUTO_MAX_DURATION] + if not self._infos[CONF_USE_MOTION_FEATURE]: + self._infos[CONF_USE_MOTION_CENTRAL_CONFIG] = False + if CONF_MOTION_SENSOR in self._infos: + del self._infos[CONF_MOTION_SENSOR] + if not self._infos[CONF_USE_POWER_FEATURE]: + self._infos[CONF_USE_POWER_CENTRAL_CONFIG] = False + if CONF_POWER_SENSOR in self._infos: + del self._infos[CONF_POWER_SENSOR] + if CONF_MAX_POWER_SENSOR in self._infos: + del self._infos[CONF_MAX_POWER_SENSOR] + if not self._infos[CONF_USE_PRESENCE_FEATURE]: + self._infos[CONF_USE_PRESENCE_CENTRAL_CONFIG] = False + if CONF_PRESENCE_SENSOR in self._infos: + del self._infos[CONF_PRESENCE_SENSOR] + if not self._infos[CONF_USE_CENTRAL_BOILER_FEATURE]: + if CONF_CENTRAL_BOILER_ACTIVATION_SRV in self._infos: + del self._infos[CONF_CENTRAL_BOILER_ACTIVATION_SRV] + if CONF_CENTRAL_BOILER_DEACTIVATION_SRV in self._infos: + del self._infos[CONF_CENTRAL_BOILER_DEACTIVATION_SRV] + if not self._infos[CONF_USE_AUTO_START_STOP_FEATURE]: + self._infos[CONF_AUTO_START_STOP_LEVEL] = AUTO_START_STOP_LEVEL_NONE + + # Removes temporary value + if COMES_FROM in self._infos: + del self._infos[COMES_FROM] + + +class VersatileThermostatConfigFlow( # pylint: disable=abstract-method + VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN +): + """Handle a config flow for Versatile Thermostat.""" + + def __init__(self) -> None: + # self._info = dict() + super().__init__(dict()) + _LOGGER.debug("CTOR ConfigFlow") + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry): + """Get options flow for this handler""" + # #713 doesn't work as explained here:https://developers.home-assistant.io/blog/2024/11/12/options-flow + # should be - return VersatileThermostatOptionsFlowHandler() but hass is not initialized + return VersatileThermostatOptionsFlowHandler(config_entry) + + async def async_step_finalize(self, _): + """Finalization of the ConfigEntry creation""" + _LOGGER.debug("ConfigFlow.async_finalize") + await super().async_step_finalize(_) + + return self.async_create_entry(title=self._infos[CONF_NAME], data=self._infos) + + +class VersatileThermostatOptionsFlowHandler( + VersatileThermostatBaseConfigFlow, OptionsFlow +): + """Handle options flow for Versatile Thermostat integration.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + + self._conf_app_id: str | None = None + + super().__init__(config_entry.data.copy()) + # #713 + # self.config_entry = config_entry + _LOGGER.debug( + "CTOR VersatileThermostatOptionsFlowHandler info: %s, entry_id: %s", + self._infos, + config_entry.entry_id, + ) + + async def async_step_init(self, user_input=None): + """Manage basic options.""" + _LOGGER.debug( + "Into OptionsFlowHandler.async_step_init user_input =%s", + user_input, + ) + + self._placeholders = { + CONF_NAME: self._infos[CONF_NAME], + PLUGINS_LINK_PLACEHOLDER: PLUGINS_LINK, + } + + return await self.async_step_menu(user_input) + + async def async_step_finalize(self, _): + """Finalization of the ConfigEntry creation""" + + _LOGGER.info( + "Recreating entry %s due to configuration change. New config is now: %s", + self.config_entry.entry_id, + self._infos, + ) + await super().async_step_finalize(_) + + self.hass.config_entries.async_update_entry(self.config_entry, data=self._infos) + return self.async_create_entry(title=None, data=None) diff --git a/custom_components/versatile_thermostat/config_schema.py b/custom_components/versatile_thermostat/config_schema.py new file mode 100644 index 0000000..0eb4b8d --- /dev/null +++ b/custom_components/versatile_thermostat/config_schema.py @@ -0,0 +1,588 @@ +""" All the schemas for ConfigFlow validation""" + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import selector +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.input_boolean import ( + DOMAIN as INPUT_BOOLEAN_DOMAIN, +) + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.input_number import ( + DOMAIN as INPUT_NUMBER_DOMAIN, +) + +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, +) + +from homeassistant.components.input_select import ( + DOMAIN as INPUT_SELECT_DOMAIN, +) + +from homeassistant.components.input_datetime import ( + DOMAIN as INPUT_DATETIME_DOMAIN, +) + +from homeassistant.components.person import DOMAIN as PERSON_DOMAIN +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN + + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import + + +def get_prop_function_options() -> list[str]: + """Return proportional algorithm choices exposed in VT flows.""" + options = [PROPORTIONAL_FUNCTION_TPI] + + try: + from .vtherm_central_api import VersatileThermostatAPI # pylint: disable=import-outside-toplevel + + api = VersatileThermostatAPI.get_vtherm_api() + if api is not None and hasattr(api, "list_prop_algorithms"): + for name in api.list_prop_algorithms(): + if name not in options: + options.append(name) + except Exception: # pylint: disable=broad-except + pass + + return options + + +def build_step_thermostat_switch_schema() -> vol.Schema: + """Build the proportional switch thermostat schema with dynamic algorithms.""" + return vol.Schema( + { + vol.Required(CONF_UNDERLYING_LIST): selector.EntitySelector( + selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN, SELECT_DOMAIN, INPUT_SELECT_DOMAIN, CLIMATE_DOMAIN], multiple=True), + ), + vol.Optional(CONF_HEATER_KEEP_ALIVE): cv.positive_int, + vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In( + get_prop_function_options() + ), + vol.Optional(CONF_AC_MODE, default=False): cv.boolean, + vol.Optional(CONF_INVERSE_SWITCH, default=False): cv.boolean, + vol.Optional("on_command_text"): vol.In([]), + vol.Optional(CONF_VSWITCH_ON_CMD_LIST): selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT, multiple=True)), + vol.Optional("off_command_text"): vol.In([]), + vol.Optional(CONF_VSWITCH_OFF_CMD_LIST): selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT, multiple=True)), + } + ) + + +def build_step_thermostat_valve_schema() -> vol.Schema: + """Build the valve thermostat schema with dynamic algorithms.""" + return vol.Schema( + { + 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( + get_prop_function_options() + ), + vol.Optional(CONF_AC_MODE, default=False): cv.boolean, + vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=10): vol.Coerce(float), + vol.Optional(CONF_AUTO_REGULATION_PERIOD_MIN, default=5): cv.positive_int, + } + ) + + +def build_step_valve_regulation_schema() -> vol.Schema: + """Build the valve regulation schema with dynamic algorithms.""" + return vol.Schema( + { + vol.Required(CONF_OPENING_DEGREE_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( + get_prop_function_options() + ), + vol.Optional(CONF_OPENING_THRESHOLD_DEGREE, default=0): cv.positive_int, + vol.Optional(CONF_MIN_OPENING_DEGREES, default=""): str, + vol.Optional(CONF_MAX_OPENING_DEGREES, default=""): str, + vol.Optional(CONF_MAX_CLOSING_DEGREE, default=100): cv.positive_int, + } + ) + +STEP_USER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required( + CONF_THERMOSTAT_TYPE, default=CONF_THERMOSTAT_SWITCH + ): selector.SelectSelector( + selector.SelectSelectorConfig( + options=CONF_THERMOSTAT_TYPES, + translation_key="thermostat_type", + mode="list", + ) + ) + } +) + +STEP_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_TEMP_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN, NUMBER_DOMAIN]), + ), + vol.Optional(CONF_LAST_SEEN_TEMP_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_DATETIME_DOMAIN, NUMBER_DOMAIN]), + ), + vol.Required(CONF_CYCLE_MIN, default=5): selector.NumberSelector(selector.NumberSelectorConfig(min=1, max=1000, step=1, mode=selector.NumberSelectorMode.BOX)), + + vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float), + vol.Required(CONF_USE_MAIN_CENTRAL_CONFIG, default=True): cv.boolean, + vol.Optional(CONF_USE_CENTRAL_MODE, default=True): cv.boolean, + vol.Required(CONF_USED_BY_CENTRAL_BOILER, default=False): cv.boolean, + } +) + +STEP_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_HEATING_FAILURE_DETECTION_FEATURE, default=False): cv.boolean, + } +) + +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_CLIMATE_VALVE_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_HEATING_FAILURE_DETECTION_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, + 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_CENTRAL_BOILER_FEATURE, default=False): cv.boolean, + vol.Optional(CONF_USE_HEATING_FAILURE_DETECTION_FEATURE, default=False): cv.boolean, + } +) + +STEP_CENTRAL_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]), + ), + vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float), + vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float), + + vol.Required(CONF_STEP_TEMPERATURE, default=0.1): vol.Coerce(float), + } +) + +STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]), + ), + vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float), + vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float), + + vol.Required(CONF_STEP_TEMPERATURE, default=0.1): vol.Coerce(float), + } +) + +STEP_CENTRAL_BOILER_SCHEMA = vol.Schema( + { + vol.Required(CONF_CENTRAL_BOILER_ACTIVATION_DELAY_SEC, default=0): selector.NumberSelector( + selector.NumberSelectorConfig(min=0, max=600, step=10, mode=selector.NumberSelectorMode.BOX) + ), + vol.Optional(CONF_CENTRAL_BOILER_ACTIVATION_SRV, default=""): str, + vol.Optional(CONF_CENTRAL_BOILER_DEACTIVATION_SRV, default=""): str, + vol.Optional(CONF_KEEP_ALIVE_BOILER_DELAY_SEC, default=DEFAULT_KEEP_ALIVE_BOILER_DELAY_SEC): selector.NumberSelector( + selector.NumberSelectorConfig(min=0, max=3600, step=10, mode=selector.NumberSelectorMode.BOX, unit_of_measurement="s") + ), + } +) + +STEP_THERMOSTAT_SWITCH = build_step_thermostat_switch_schema() # pylint: disable=invalid-name + +STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name + { + 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(CONF_SYNC_DEVICE_INTERNAL_TEMP, default=False): cv.boolean, + vol.Optional(CONF_AUTO_REGULATION_MODE, default=CONF_AUTO_REGULATION_NONE): selector.SelectSelector( + selector.SelectSelectorConfig( + options=CONF_AUTO_REGULATION_MODES, + translation_key="auto_regulation_mode", + mode="dropdown", + ) + ), + vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=0.5): vol.Coerce(float), + vol.Optional(CONF_AUTO_REGULATION_PERIOD_MIN, default=5): cv.positive_int, + vol.Optional(CONF_AUTO_FAN_MODE, default=CONF_AUTO_FAN_HIGH): selector.SelectSelector( + selector.SelectSelectorConfig( + options=CONF_AUTO_FAN_MODES, + translation_key="auto_fan_mode", + mode="dropdown", + ) + ), + vol.Optional(CONF_AUTO_REGULATION_USE_DEVICE_TEMP, default=False): cv.boolean, + } +) + +STEP_THERMOSTAT_VALVE = build_step_thermostat_valve_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 = build_step_valve_regulation_schema() # pylint: disable=invalid-name + +STEP_SYNC_DEVICE_INTERNAL_TEMP = vol.Schema( # pylint: disable=invalid-name + { + vol.Optional(CONF_SYNC_WITH_CALIBRATION, default=True): cv.boolean, + vol.Required(CONF_SYNC_ENTITY_LIST): selector.EntitySelector( + selector.EntitySelectorConfig(domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True), + ), + } +) + +STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_USE_TPI_CENTRAL_CONFIG, default=True): cv.boolean, + } +) + +STEP_CENTRAL_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_TPI_COEF_INT, default=0.6): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0.0, max=10.0, step="any", mode=selector.NumberSelectorMode.BOX + ) + ), + vol.Required(CONF_TPI_COEF_EXT, default=0.01): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0.0, max=1.0, step="any", mode=selector.NumberSelectorMode.BOX + ) + ), + vol.Required(CONF_MINIMAL_ACTIVATION_DELAY, default=10): cv.positive_int, + vol.Required(CONF_MINIMAL_DEACTIVATION_DELAY, default=0): cv.positive_int, + vol.Optional(CONF_TPI_THRESHOLD_LOW, default=0): selector.NumberSelector( + selector.NumberSelectorConfig( + min=-10.0, max=10.0, step=0.1, mode=selector.NumberSelectorMode.BOX + ) + ), + vol.Optional(CONF_TPI_THRESHOLD_HIGH, default=0): selector.NumberSelector( + selector.NumberSelectorConfig( + min=-10.0, max=10.0, step=0.1, mode=selector.NumberSelectorMode.BOX + ) + ), + vol.Optional(CONF_AUTO_TPI_MODE, default=False): cv.boolean, + } +) +# Duplicating schema for central config +# as we don't want to display the use_auto_tpi checkbox +# in central config but only in device config. +STEP_CENTRAL_TPI_DATA_SCHEMA_CENTRAL = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_TPI_COEF_INT, default=0.6): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0.0, max=10.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.001, mode=selector.NumberSelectorMode.BOX + ) + ), + vol.Required(CONF_MINIMAL_ACTIVATION_DELAY, default=10): cv.positive_int, + vol.Required(CONF_MINIMAL_DEACTIVATION_DELAY, default=0): cv.positive_int, + vol.Optional(CONF_TPI_THRESHOLD_LOW, default=0): selector.NumberSelector( + selector.NumberSelectorConfig( + min=-10.0, max=10.0, step=0.1, mode=selector.NumberSelectorMode.BOX + ) + ), + vol.Optional(CONF_TPI_THRESHOLD_HIGH, default=0): selector.NumberSelector( + selector.NumberSelectorConfig( + min=-10.0, max=10.0, step=0.1, mode=selector.NumberSelectorMode.BOX + ) + ), + } +) + +STEP_PRESETS_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_USE_PRESETS_CENTRAL_CONFIG, default=True): cv.boolean, + } +) + +STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Optional(CONF_WINDOW_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig( + domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN] + ), + ), + vol.Required(CONF_USE_WINDOW_CENTRAL_CONFIG, default=True): cv.boolean, + } +) + +STEP_CENTRAL_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int, + vol.Optional(CONF_WINDOW_OFF_DELAY, default=30): cv.positive_int, + vol.Optional(CONF_WINDOW_AUTO_OPEN_THRESHOLD, default=3): vol.Coerce(float), + vol.Optional(CONF_WINDOW_AUTO_CLOSE_THRESHOLD, default=0): vol.Coerce(float), + vol.Optional(CONF_WINDOW_AUTO_MAX_DURATION, default=30): cv.positive_int, + vol.Optional(CONF_WINDOW_ACTION, default=CONF_WINDOW_TURN_OFF): selector.SelectSelector( + selector.SelectSelectorConfig( + options=CONF_WINDOW_ACTIONS, + translation_key="window_action", + mode="dropdown", + ) + ), + } +) + +STEP_CENTRAL_WINDOW_WO_AUTO_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int, + vol.Optional(CONF_WINDOW_OFF_DELAY, default=30): cv.positive_int, + vol.Optional(CONF_WINDOW_ACTION, default=CONF_WINDOW_TURN_OFF): selector.SelectSelector( + selector.SelectSelectorConfig( + options=CONF_WINDOW_ACTIONS, + translation_key="window_action", + mode="dropdown", + ) + ), + } +) + +STEP_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_MOTION_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig( + domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN] + ), + ), + vol.Required(CONF_USE_MOTION_CENTRAL_CONFIG, default=True): cv.boolean, + } +) + +STEP_CENTRAL_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int, + vol.Optional(CONF_MOTION_OFF_DELAY, default=300): cv.positive_int, + vol.Optional(CONF_MOTION_PRESET, default="comfort"): selector.SelectSelector( + selector.SelectSelectorConfig( + options=CONF_PRESETS_SELECTIONABLE, + translation_key="presets", + mode="dropdown", + ) + ), + vol.Optional(CONF_NO_MOTION_PRESET, default="eco"): selector.SelectSelector( + selector.SelectSelectorConfig( + options=CONF_PRESETS_SELECTIONABLE, + translation_key="presets", + mode="dropdown", + ) + ), + } +) + +STEP_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_POWER_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]), + ), + vol.Required(CONF_MAX_POWER_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]), + ), + vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float), + } +) + +STEP_NON_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float), + } +) + +STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_USE_POWER_CENTRAL_CONFIG, default=True): cv.boolean, + } +) + +STEP_CENTRAL_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_PRESENCE_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig( + domain=[ + PERSON_DOMAIN, + BINARY_SENSOR_DOMAIN, + INPUT_BOOLEAN_DOMAIN, + ] + ), + ) + }, +) + +STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_USE_PRESENCE_CENTRAL_CONFIG, default=True): cv.boolean, + } +) + +STEP_CENTRAL_ADVANCED_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_SAFETY_DELAY_MIN, default=60): cv.positive_int, + vol.Required( + CONF_SAFETY_MIN_ON_PERCENT, + default=DEFAULT_SAFETY_MIN_ON_PERCENT, + ): vol.Coerce(float), + vol.Required( + CONF_SAFETY_DEFAULT_ON_PERCENT, + default=DEFAULT_SAFETY_DEFAULT_ON_PERCENT, + ): vol.Coerce(float), + vol.Optional(CONF_REPAIR_INCORRECT_STATE, default=False): cv.boolean, + } +) + +STEP_ADVANCED_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_USE_ADVANCED_CENTRAL_CONFIG, default=True): cv.boolean, + } +) + +STEP_LOCK_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_USE_LOCK_CENTRAL_CONFIG, default=True): cv.boolean, + } +) + +STEP_CENTRAL_LOCK_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Optional(CONF_LOCK_CODE): cv.string, + vol.Optional(CONF_LOCK_USERS, default=True): cv.boolean, + vol.Optional(CONF_LOCK_AUTOMATIONS, default=True): cv.boolean, + vol.Optional(CONF_AUTO_RELOCK_SEC, default=30): vol.Coerce(int), + } +) + +STEP_CENTRAL_HEATING_FAILURE_DETECTION_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required( + CONF_HEATING_FAILURE_THRESHOLD, + default=DEFAULT_HEATING_FAILURE_THRESHOLD, + ): selector.NumberSelector(selector.NumberSelectorConfig(min=0.0, max=1.0, step=0.01, mode=selector.NumberSelectorMode.BOX)), + vol.Required( + CONF_COOLING_FAILURE_THRESHOLD, + default=DEFAULT_COOLING_FAILURE_THRESHOLD, + ): selector.NumberSelector(selector.NumberSelectorConfig(min=0.0, max=1.0, step=0.01, mode=selector.NumberSelectorMode.BOX)), + vol.Required( + CONF_HEATING_FAILURE_DETECTION_DELAY, + default=DEFAULT_HEATING_FAILURE_DETECTION_DELAY, + ): cv.positive_int, + vol.Required( + CONF_TEMPERATURE_CHANGE_TOLERANCE, + default=DEFAULT_TEMPERATURE_CHANGE_TOLERANCE, + ): selector.NumberSelector(selector.NumberSelectorConfig(min=0.0, max=5.0, step=0.1, mode=selector.NumberSelectorMode.BOX)), + vol.Optional( + CONF_FAILURE_DETECTION_ENABLE_TEMPLATE, + ): selector.TemplateSelector(), + } +) + +STEP_HEATING_FAILURE_DETECTION_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_USE_HEATING_FAILURE_DETECTION_CENTRAL_CONFIG, default=True): cv.boolean, + } +) + +STEP_AUTO_TPI_CONFIGURATION_SCHEMA = vol.Schema( + { + vol.Required( + CONF_AUTO_TPI_LEARNING_TYPE, default=AUTO_TPI_LEARNING_TYPE_DISCOVERY + ): selector.SelectSelector( + selector.SelectSelectorConfig( + options=CONF_AUTO_TPI_LEARNING_TYPES, + translation_key="auto_tpi_learning_type", + mode="dropdown", + ) + ), + vol.Required(CONF_AUTO_TPI_AGGRESSIVENESS, default=1.0): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0.5, max=1.0, step=0.01, mode=selector.NumberSelectorMode.SLIDER + ) + ), + vol.Optional(CONF_AUTO_TPI_HEATER_HEATING_TIME, default=5): cv.positive_int, + vol.Optional(CONF_AUTO_TPI_HEATER_COOLING_TIME, default=5): cv.positive_int, + vol.Required(CONF_AUTO_TPI_HEATING_POWER, default=0.0): vol.Coerce(float), + vol.Optional(CONF_AUTO_TPI_CONTINUOUS_KEXT, default=False): cv.boolean, + vol.Optional( + CONF_AUTO_TPI_ENABLE_ADVANCED_SETTINGS, default=False + ): cv.boolean, + } +) + + +STEP_AUTO_TPI_AVG_SETTINGS_SCHEMA = vol.Schema( + { + vol.Required(CONF_AUTO_TPI_AVG_INITIAL_WEIGHT, default=1): cv.positive_int, + } +) + +STEP_AUTO_TPI_EMA_SETTINGS_SCHEMA = vol.Schema( + { + vol.Required(CONF_AUTO_TPI_EMA_ALPHA, default=0.15): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0.0, max=1.0, step=0.01, mode=selector.NumberSelectorMode.BOX + ) + ), + vol.Required( + CONF_AUTO_TPI_EMA_DECAY_RATE, default=0.08 + ): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0.0, max=1.0, step=0.01, mode=selector.NumberSelectorMode.BOX + ) + ), + vol.Required(CONF_AUTO_TPI_CONTINUOUS_KEXT_ALPHA, default=0.04): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0.005, max=0.2, step=0.005, mode=selector.NumberSelectorMode.BOX + ) + ), + } +) diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py new file mode 100644 index 0000000..b2ec354 --- /dev/null +++ b/custom_components/versatile_thermostat/const.py @@ -0,0 +1,691 @@ +# pylint: disable=line-too-long, disable=unused-import +"""Constants for the Versatile Thermostat integration.""" + +import logging +import math +from typing import Literal + +from datetime import datetime + +from enum import Enum +from homeassistant.const import STATE_UNKNOWN, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_NAME, Platform + +from homeassistant.components.climate.const import ClimateEntityFeature # pylint: disable=unused-import +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt as dt_util + + +from vtherm_api.const import DOMAIN, VTHERM_API_NAME, DEVICE_MANUFACTURER, DEVICE_MODEL, get_tz, NowClass, EventType +from vtherm_api.log_collector import get_vtherm_logger + +PROPORTIONAL_FUNCTION_TPI = "tpi" + +from .vtherm_preset import VThermPreset, VThermPresetWithAC, VThermPresetWithAway, VThermPresetWithACAway, PRESET_TEMP_SUFFIX, PRESET_AWAY_SUFFIX # pylint: disable=unused-import +from .vtherm_hvac_mode import ( + VThermHvacMode, + VThermHvacMode_COOL, + VThermHvacMode_HEAT, + VThermHvacMode_DRY, + VThermHvacMode_OFF, + VThermHvacMode_HEAT_COOL, + VThermHvacMode_SLEEP, + VThermHvacMode_AUTO, + VThermHvacMode_FAN_ONLY, + to_ha_hvac_mode, + from_ha_hvac_mode, +) # pylint: disable=unused-import +from .vtherm_state import VThermState # pylint: disable=unused-import + +_LOGGER = get_vtherm_logger(__name__) + +CONFIG_VERSION = 2 +CONFIG_MINOR_VERSION = 3 + +DEVICE_MANUFACTURER = "JMCOLLIN" +DEVICE_MODEL = "Versatile Thermostat" + +DOMAIN = "versatile_thermostat" + +# The order is important. +PLATFORMS: list[Platform] = [ + Platform.SELECT, + Platform.CLIMATE, + Platform.SENSOR, + # Number should be after CLIMATE + Platform.NUMBER, + Platform.BINARY_SENSOR, + Platform.SWITCH, +] + +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" +CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id" +CONF_POWER_SENSOR = "power_sensor_entity_id" +CONF_MAX_POWER_SENSOR = "max_power_sensor_entity_id" +CONF_WINDOW_SENSOR = "window_sensor_entity_id" +CONF_MOTION_SENSOR = "motion_sensor_entity_id" +CONF_DEVICE_POWER = "device_power" +CONF_CYCLE_MIN = "cycle_min" +CONF_PROP_FUNCTION = "proportional_function" +CONF_WINDOW_DELAY = "window_delay" +CONF_WINDOW_OFF_DELAY = "window_off_delay" +CONF_MOTION_DELAY = "motion_delay" +CONF_MOTION_OFF_DELAY = "motion_off_delay" +CONF_MOTION_PRESET = "motion_preset" +CONF_NO_MOTION_PRESET = "no_motion_preset" +CONF_TPI_COEF_INT = "tpi_coef_int" +CONF_TPI_COEF_EXT = "tpi_coef_ext" +CONF_TPI_THRESHOLD_LOW = "tpi_threshold_low" +CONF_TPI_THRESHOLD_HIGH = "tpi_threshold_high" +CONF_PRESENCE_SENSOR = "presence_sensor_entity_id" +CONF_PRESET_POWER = "power_temp" +CONF_MINIMAL_ACTIVATION_DELAY = "minimal_activation_delay" +CONF_MINIMAL_DEACTIVATION_DELAY = "minimal_deactivation_delay" +CONF_TEMP_MIN = "temp_min" +CONF_TEMP_MAX = "temp_max" +CONF_SAFETY_DELAY_MIN = "safety_delay_min" +CONF_SAFETY_MIN_ON_PERCENT = "safety_min_on_percent" +CONF_SAFETY_DEFAULT_ON_PERCENT = "safety_default_on_percent" +CONF_THERMOSTAT_TYPE = "thermostat_type" +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_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_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" +CONF_AUTO_REGULATION_STRONG = "auto_regulation_strong" +CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert" +CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp" +CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min" +CONF_AUTO_REGULATION_USE_DEVICE_TEMP = "auto_regulation_use_device_temp" +CONF_INVERSE_SWITCH = "inverse_switch_command" +CONF_AUTO_FAN_MODE = "auto_fan_mode" +CONF_AUTO_FAN_NONE = "auto_fan_none" +CONF_AUTO_FAN_LOW = "auto_fan_low" +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" +CONF_MIN_OPENING_DEGREES = "min_opening_degrees" +CONF_MAX_OPENING_DEGREES = "max_opening_degrees" +CONF_MAX_CLOSING_DEGREE = "max_closing_degree" +CONF_OPENING_THRESHOLD_DEGREE = "opening_threshold_degree" + +CONF_SYNC_DEVICE_INTERNAL_TEMP = "sync_device_internal_temp" +CONF_SYNC_WITH_CALIBRATION = "sync_with_calibration" +CONF_SYNC_ENTITY_LIST = "sync_entity_ids" + +CONF_USE_HEATING_FAILURE_DETECTION_FEATURE = "use_heating_failure_detection_feature" +CONF_USE_HEATING_FAILURE_DETECTION_CENTRAL_CONFIG = "use_heating_failure_detection_central_config" +CONF_HEATING_FAILURE_THRESHOLD = "heating_failure_threshold" +CONF_COOLING_FAILURE_THRESHOLD = "cooling_failure_threshold" +CONF_HEATING_FAILURE_DETECTION_DELAY = "heating_failure_detection_delay" +CONF_TEMPERATURE_CHANGE_TOLERANCE = "temperature_change_tolerance" +CONF_FAILURE_DETECTION_ENABLE_TEMPLATE = "failure_detection_enable_template" + +CONF_LOCK_CODE = "lock_code" +CONF_LOCK_USERS = "lock_users" +CONF_LOCK_AUTOMATIONS = "lock_automations" +CONF_AUTO_RELOCK_SEC = "auto_relock_sec" + +CONF_REPAIR_INCORRECT_STATE = "repair_incorrect_state" + +CONF_VSWITCH_ON_CMD_LIST = "vswitch_on_command" +CONF_VSWITCH_OFF_CMD_LIST = "vswitch_off_command" + +# 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" + +CONF_AUTO_TPI_MODE = "auto_tpi_mode" +CONF_AUTO_TPI_LEARNING_TYPE = "auto_tpi_learning_type" +AUTO_TPI_LEARNING_TYPE_DISCOVERY = "discovery" +AUTO_TPI_LEARNING_TYPE_FINE_TUNING = "fine_tuning" +CONF_AUTO_TPI_LEARNING_TYPES = [ + AUTO_TPI_LEARNING_TYPE_DISCOVERY, + AUTO_TPI_LEARNING_TYPE_FINE_TUNING, +] +CONF_AUTO_TPI_ENABLE_ADVANCED_SETTINGS = "auto_tpi_enable_advanced_settings" +CONF_AUTO_TPI_HEATER_HEATING_TIME = "heater_heating_time" +CONF_AUTO_TPI_HEATER_COOLING_TIME = "heater_cooling_time" +CONF_AUTO_TPI_CALCULATION_METHOD = "auto_tpi_calculation_method" +AUTO_TPI_METHOD_AVG = "average" +AUTO_TPI_METHOD_EMA = "ema" +CONF_AUTO_TPI_CALCULATION_METHODS = [AUTO_TPI_METHOD_AVG, AUTO_TPI_METHOD_EMA] +CONF_AUTO_TPI_EMA_ALPHA = "auto_tpi_ema_alpha" +CONF_AUTO_TPI_AVG_INITIAL_WEIGHT = "auto_tpi_avg_initial_weight" + +CONF_AUTO_TPI_HEATING_POWER = "auto_tpi_heating_rate" +CONF_AUTO_TPI_COOLING_POWER = "auto_tpi_cooling_rate" +CONF_AUTO_TPI_AGGRESSIVENESS = "auto_tpi_aggressiveness" + +CONF_AUTO_TPI_EMA_DECAY_RATE = "auto_tpi_ema_decay_rate" +CONF_AUTO_TPI_CONTINUOUS_KEXT = "auto_tpi_continuous_kext" +CONF_AUTO_TPI_CONTINUOUS_KEXT_ALPHA = "auto_tpi_continuous_kext_alpha" + + +# 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_LOG_BUFFER_MAX_AGE_HOURS = "log_buffer_max_age_hours" + +CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config" +CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config" +CONF_USE_WINDOW_CENTRAL_CONFIG = "use_window_central_config" +CONF_USE_MOTION_CENTRAL_CONFIG = "use_motion_central_config" +CONF_USE_POWER_CENTRAL_CONFIG = "use_power_central_config" +CONF_USE_PRESENCE_CENTRAL_CONFIG = "use_presence_central_config" +CONF_USE_PRESETS_CENTRAL_CONFIG = "use_presets_central_config" +CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config" +CONF_USE_LOCK_CENTRAL_CONFIG = "use_lock_central_config" + +CONF_USE_CENTRAL_MODE = "use_central_mode" + +CONF_CENTRAL_BOILER_ACTIVATION_SRV = "central_boiler_activation_service" +CONF_CENTRAL_BOILER_DEACTIVATION_SRV = "central_boiler_deactivation_service" +CONF_CENTRAL_BOILER_ACTIVATION_DELAY_SEC = "central_boiler_activation_delay_sec" +CONF_KEEP_ALIVE_BOILER_DELAY_SEC = "keep_alive_boiler_delay_sec" + +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_VERY_SLOW = "auto_start_stop_very_slow" +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_VERY_SLOW, + 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_VERY_SLOW, + AUTO_START_STOP_LEVEL_NONE, +] + +HVAC_OFF_REASON_NAME = "hvac_off_reason" +HVAC_OFF_REASON_MANUAL = "hvac_off_manual" +HVAC_OFF_REASON_AUTO_START_STOP = "hvac_off_auto_start_stop" +HVAC_OFF_REASON_WINDOW_DETECTION = "hvac_off_window_detection" +HVAC_OFF_REASON_SLEEP_MODE = "hvac_off_sleep_mode" +HVAC_OFF_REASON_SAFETY = "hvac_off_safety_detection" +HVAC_OFF_REASON_CENTRAL_MODE = "hvac_off_central_mode" +HVAC_OFF_REASONS = Literal[ # pylint: disable=invalid-name + HVAC_OFF_REASON_MANUAL, HVAC_OFF_REASON_AUTO_START_STOP, HVAC_OFF_REASON_WINDOW_DETECTION, HVAC_OFF_REASON_SLEEP_MODE, HVAC_OFF_REASON_SAFETY +] + +DEFAULT_SHORT_EMA_PARAMS = { + "max_alpha": 0.5, + # In sec + "halflife_sec": 300, + "precision": 2, +} + +CONF_PRESETS = { + p: f"{p}{PRESET_TEMP_SUFFIX}" + for p in ( + VThermPreset.FROST, + VThermPreset.ECO, + VThermPreset.COMFORT, + VThermPreset.BOOST, + ) +} + +CONF_PRESETS_WITH_AC = { + p: f"{p}{PRESET_TEMP_SUFFIX}" + for p in ( + VThermPreset.FROST, + VThermPreset.ECO, + VThermPreset.COMFORT, + VThermPreset.BOOST, + VThermPresetWithAC.ECO, + VThermPresetWithAC.COMFORT, + VThermPresetWithAC.BOOST, + ) +} + +CONF_PRESETS_AWAY = { + p: f"{p}{PRESET_TEMP_SUFFIX}" + for p in ( + VThermPresetWithAway.FROST, + VThermPresetWithAway.ECO, + VThermPresetWithAway.COMFORT, + VThermPresetWithAway.BOOST, + ) +} + +CONF_PRESETS_AWAY_WITH_AC = { + p: f"{p}{PRESET_TEMP_SUFFIX}" + for p in ( + VThermPresetWithAway.FROST, + VThermPresetWithAway.ECO, + VThermPresetWithAway.COMFORT, + VThermPresetWithAway.BOOST, + VThermPresetWithACAway.ECO, + VThermPresetWithACAway.COMFORT, + VThermPresetWithACAway.BOOST, + ) +} + +CONF_PRESETS_SELECTIONABLE = [ + VThermPreset.FROST, + VThermPreset.ECO, + VThermPreset.COMFORT, + VThermPreset.BOOST, +] + +CONF_PRESETS_VALUES = list(CONF_PRESETS.values()) +CONF_PRESETS_AWAY_VALUES = list(CONF_PRESETS_AWAY.values()) +CONF_PRESETS_WITH_AC_VALUES = list(CONF_PRESETS_WITH_AC.values()) +CONF_PRESETS_AWAY_WITH_AC_VALUES = list(CONF_PRESETS_AWAY_WITH_AC.values()) + +ALL_CONF = ( + [ + CONF_NAME, + CONF_HEATER_KEEP_ALIVE, + CONF_TEMP_SENSOR, + CONF_EXTERNAL_TEMP_SENSOR, + CONF_POWER_SENSOR, + CONF_MAX_POWER_SENSOR, + CONF_WINDOW_SENSOR, + CONF_WINDOW_DELAY, + CONF_WINDOW_OFF_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, + CONF_NO_MOTION_PRESET, + CONF_DEVICE_POWER, + CONF_CYCLE_MIN, + CONF_PROP_FUNCTION, + CONF_TPI_COEF_INT, + CONF_TPI_COEF_EXT, + CONF_TPI_THRESHOLD_LOW, + CONF_TPI_THRESHOLD_HIGH, + CONF_AUTO_TPI_HEATER_HEATING_TIME, + CONF_AUTO_TPI_HEATER_COOLING_TIME, + CONF_PRESENCE_SENSOR, + CONF_MINIMAL_ACTIVATION_DELAY, + CONF_MINIMAL_DEACTIVATION_DELAY, + CONF_TEMP_MIN, + CONF_TEMP_MAX, + CONF_SAFETY_DELAY_MIN, + CONF_SAFETY_MIN_ON_PERCENT, + CONF_SAFETY_DEFAULT_ON_PERCENT, + CONF_THERMOSTAT_TYPE, + CONF_THERMOSTAT_SWITCH, + CONF_THERMOSTAT_CLIMATE, + 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_AUTO_REGULATION_MODE, + CONF_AUTO_REGULATION_DTEMP, + CONF_AUTO_REGULATION_PERIOD_MIN, + CONF_AUTO_REGULATION_USE_DEVICE_TEMP, + CONF_INVERSE_SWITCH, + CONF_AUTO_FAN_MODE, + 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_CENTRAL_MODE, + CONF_USED_BY_CENTRAL_BOILER, + CONF_CENTRAL_BOILER_ACTIVATION_SRV, + CONF_CENTRAL_BOILER_DEACTIVATION_SRV, + CONF_CENTRAL_BOILER_ACTIVATION_DELAY_SEC, + CONF_KEEP_ALIVE_BOILER_DELAY_SEC, + CONF_WINDOW_ACTION, + CONF_STEP_TEMPERATURE, + CONF_MIN_OPENING_DEGREES, + CONF_MAX_OPENING_DEGREES, + CONF_MAX_CLOSING_DEGREE, + CONF_OPENING_THRESHOLD_DEGREE, + CONF_AUTO_TPI_CALCULATION_METHOD, + CONF_AUTO_TPI_EMA_ALPHA, + CONF_AUTO_TPI_AVG_INITIAL_WEIGHT, + CONF_AUTO_TPI_HEATING_POWER, + CONF_AUTO_TPI_COOLING_POWER, + CONF_AUTO_TPI_EMA_DECAY_RATE, + CONF_AUTO_TPI_CONTINUOUS_KEXT, + CONF_AUTO_TPI_CONTINUOUS_KEXT_ALPHA, + CONF_AUTO_TPI_LEARNING_TYPE, + CONF_AUTO_TPI_ENABLE_ADVANCED_SETTINGS, + CONF_SYNC_DEVICE_INTERNAL_TEMP, + CONF_SYNC_WITH_CALIBRATION, + CONF_USE_HEATING_FAILURE_DETECTION_FEATURE, + CONF_USE_HEATING_FAILURE_DETECTION_CENTRAL_CONFIG, + CONF_HEATING_FAILURE_THRESHOLD, + CONF_COOLING_FAILURE_THRESHOLD, + CONF_HEATING_FAILURE_DETECTION_DELAY, + CONF_TEMPERATURE_CHANGE_TOLERANCE, + CONF_FAILURE_DETECTION_ENABLE_TEMPLATE, + CONF_REPAIR_INCORRECT_STATE, + ] + + CONF_PRESETS_VALUES + + CONF_PRESETS_AWAY_VALUES + + CONF_PRESETS_WITH_AC_VALUES + + CONF_PRESETS_AWAY_WITH_AC_VALUES, +) + +CONF_FUNCTIONS = [ + PROPORTIONAL_FUNCTION_TPI, +] + +CONF_AUTO_REGULATION_MODES = [ + CONF_AUTO_REGULATION_NONE, + CONF_AUTO_REGULATION_VALVE, + CONF_AUTO_REGULATION_LIGHT, + CONF_AUTO_REGULATION_MEDIUM, + CONF_AUTO_REGULATION_STRONG, + CONF_AUTO_REGULATION_SLOW, + CONF_AUTO_REGULATION_EXPERT, +] + +CONF_THERMOSTAT_TYPES = [ + CONF_THERMOSTAT_CENTRAL_CONFIG, + CONF_THERMOSTAT_SWITCH, + CONF_THERMOSTAT_CLIMATE, + CONF_THERMOSTAT_VALVE, +] + +CONF_AUTO_FAN_MODES = [ + CONF_AUTO_FAN_NONE, + CONF_AUTO_FAN_LOW, + CONF_AUTO_FAN_MEDIUM, + CONF_AUTO_FAN_HIGH, + CONF_AUTO_FAN_TURBO, +] + +CONF_WINDOW_TURN_OFF = "window_turn_off" +CONF_WINDOW_FAN_ONLY = "window_fan_only" +CONF_WINDOW_FROST_TEMP = "window_frost_temp" +CONF_WINDOW_ECO_TEMP = "window_eco_temp" + +CONF_WINDOW_ACTIONS = [ + CONF_WINDOW_TURN_OFF, + CONF_WINDOW_FAN_ONLY, + CONF_WINDOW_FROST_TEMP, + CONF_WINDOW_ECO_TEMP, +] + +SUPPORT_FLAGS = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON +) + +SERVICE_SET_PRESENCE = "set_presence" +SERVICE_SET_SAFETY = "set_safety" +SERVICE_SET_WINDOW_BYPASS = "set_window_bypass" +SERVICE_SET_AUTO_REGULATION_MODE = "set_auto_regulation_mode" +SERVICE_SET_AUTO_FAN_MODE = "set_auto_fan_mode" +SERVICE_SET_HVAC_MODE_SLEEP = "set_hvac_mode_sleep" +SERVICE_LOCK = "lock" +SERVICE_UNLOCK = "unlock" +SERVICE_SET_TPI_PARAMETERS = "set_tpi_parameters" +SERVICE_SET_AUTO_TPI_MODE = "set_auto_tpi_mode" +SERVICE_AUTO_TPI_CALIBRATE_CAPACITY = "auto_tpi_calibrate_capacity" +AUTO_TPI_EVENT = "versatile_thermostat_auto_tpi_event" +SERVICE_SET_TIMED_PRESET = "set_timed_preset" +SERVICE_CANCEL_TIMED_PRESET = "cancel_timed_preset" +SERVICE_RECALIBRATE_VALVES = "recalibrate_valves" +SERVICE_DOWNLOAD_LOGS = "download_logs" + +DEFAULT_SAFETY_MIN_ON_PERCENT = 0.5 +DEFAULT_SAFETY_DEFAULT_ON_PERCENT = 0.1 + +# Repair incorrect state defaults +DEFAULT_REPAIR_INCORRECT_STATE = False +REPAIR_MAX_ATTEMPTS = 5 +REPAIR_MIN_DELAY_AFTER_INIT_SEC = 30 + +# Central boiler keep-alive defaults +DEFAULT_KEEP_ALIVE_BOILER_DELAY_SEC = 0 + +# Heating failure detection defaults +DEFAULT_HEATING_FAILURE_THRESHOLD = 0.9 # 90% +DEFAULT_COOLING_FAILURE_THRESHOLD = 0.0 # 0% +DEFAULT_HEATING_FAILURE_DETECTION_DELAY = 15 # 15 minutes +DEFAULT_TEMPERATURE_CHANGE_TOLERANCE = 0.5 # 0.5°C + +ATTR_TOTAL_ENERGY = "total_energy" +ATTR_MEAN_POWER_CYCLE = "mean_cycle_power" + +AUTO_FAN_DTEMP_THRESHOLD = 2 +AUTO_FAN_DEACTIVATED_MODES = ["mute", "quiet", "low", "quiet", "1", "auto"] + +CENTRAL_CONFIG_NAME = "Central configuration" + +CENTRAL_MODE_AUTO = "Auto" +CENTRAL_MODE_STOPPED = "Stopped" +CENTRAL_MODE_HEAT_ONLY = "Heat only" +CENTRAL_MODE_COOL_ONLY = "Cool only" +CENTRAL_MODE_FROST_PROTECTION = "Frost protection" +CENTRAL_MODES = [ + CENTRAL_MODE_AUTO, + CENTRAL_MODE_STOPPED, + CENTRAL_MODE_HEAT_ONLY, + CENTRAL_MODE_COOL_ONLY, + CENTRAL_MODE_FROST_PROTECTION, +] + +ATTR_CURRENT_STATE = "current_state" +ATTR_REQUESTED_STATE = "requested_state" + +MSG_OVERPOWERING_DETECTED = "overpowering_detected" +MSG_SAFETY_DETECTED = "safety_detected" +MSG_TARGET_TEMP_WINDOW_ECO = "target_temp_window_eco" +MSG_TARGET_TEMP_WINDOW_FROST = "target_temp_window_frost" +MSG_TARGET_TEMP_POWER = "target_temp_power" +MSG_TARGET_TEMP_CENTRAL_MODE = "target_temp_central_mode" +MSG_TARGET_TEMP_ACTIVITY_DETECTED = "target_temp_activity_detected" +MSG_TARGET_TEMP_ACTIVITY_NOT_DETECTED = "target_temp_activity_not_detected" +MSG_TARGET_TEMP_ABSENCE_DETECTED = "target_temp_absence_detected" +MSG_TARGET_TEMP_TIMED_PRESET = "target_temp_timed_preset" +MSG_NOT_INITIALIZED = "not_initialized" + +# A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154 +class RegulationParamSlow: + """Light parameters for slow latency regulation""" + + kp: float = ( + 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature + ) + ki: float = ( + 0.8 / 288.0 + ) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours + k_ext: float = ( + 1.0 / 25.0 + ) # this will add 1°C to the offset when it's 25°C colder outdoor than indoor + offset_max: float = 2.0 # limit to a final offset of -2°C to +2°C + accumulated_error_threshold: float = ( + 2.0 * 288 + ) # this allows up to 2°C long term offset in both directions + overheat_protection: bool = True + +class RegulationParamLight: + """Light parameters for regulation""" + + kp: float = 0.2 + ki: float = 0.05 + k_ext: float = 0.05 + offset_max: float = 1.5 + accumulated_error_threshold: float = 10 + overheat_protection: bool = True + + +class RegulationParamMedium: + """Light parameters for regulation""" + + kp: float = 0.3 + ki: float = 0.05 + k_ext: float = 0.1 + offset_max: float = 2 + accumulated_error_threshold: float = 20 + overheat_protection: bool = True + + +class RegulationParamStrong: + """Strong parameters for regulation + A set of parameters which doesn't take into account the external temp + and concentrate to internal temp error + accumulated error. + This should work for cold external conditions which else generates + high external_offset""" + + kp: float = 0.4 + ki: float = 0.08 + k_ext: float = 0.0 + offset_max: float = 5 + accumulated_error_threshold: float = 50 + overheat_protection: bool = True + + +# Not used now +class RegulationParamVeryStrong: + """Strong parameters for regulation""" + + kp: float = 0.6 + ki: float = 0.1 + k_ext: float = 0.2 + offset_max: float = 8 + accumulated_error_threshold: float = 80 + overheat_protection: bool = True + +def send_vtherm_event(hass, event_type: EventType, entity, data: dict): + """Send an event""" + _LOGGER.info("%s - Sending event %s with data: %s", entity, event_type, data) + data["entity_id"] = entity.entity_id + data["name"] = entity.name + data["state_attributes"] = entity.state_attributes + 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 in [None, "None", STATE_UNAVAILABLE, STATE_UNKNOWN]: + return None + return get_safe_float_value(state.state) + + +def get_safe_float_value(value): + """Get a safe float value. + Return None if value is not a valid float""" + try: + float_val = float(value) + return None if math.isinf(float_val) or not math.isfinite(float_val) else float_val + except (ValueError, TypeError): + return None + + +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.""" + + +class NoCentralConfig(HomeAssistantError): + """Error to indicate that we try to use a central configuration but no VTherm of type CENTRAL CONFIGURATION has been found""" + + +class ServiceConfigurationError(HomeAssistantError): + """Error in the service configuration to control the central boiler""" + + +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 SyncDeviceInternalTempNbEntitiesIncorrect(HomeAssistantError): + """Error to indicate there is an error in the configuration of the TRV with synchronize device internal temperature. + The number of specific entities is incorrect.""" + + +class ValveRegulationMinOpeningDegreesIncorrect(HomeAssistantError): + """Error to indicate that the minimal opening degrees is not a list of int separated by coma""" + + +class ValveRegulationMaxOpeningDegreesIncorrect(HomeAssistantError): + """Error to indicate that the maximal opening degrees is not a list of int separated by coma""" + + +class ValveRegulationMinMaxOpeningDegreesIncorrect(HomeAssistantError): + """Error to indicate that max_opening_degrees must be greater than min_opening_degrees for each underlying""" + + +class VirtualSwitchConfigurationIncorrect(HomeAssistantError): + """Error when a virtual switch is not configured correctly""" + + +class LockCodeIncorrect(HomeAssistantError): + """Error when a lock code is not configured correctly""" + + +class overrides: # pylint: disable=invalid-name + """An annotation to inform overrides""" + + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + return self.func.__get__(instance, owner) + + def __call__(self, *args, **kwargs): + raise RuntimeError(f"Method {self.func.__name__} should have been overridden") diff --git a/custom_components/versatile_thermostat/cycle_scheduler.py b/custom_components/versatile_thermostat/cycle_scheduler.py new file mode 100644 index 0000000..f68fb49 --- /dev/null +++ b/custom_components/versatile_thermostat/cycle_scheduler.py @@ -0,0 +1,783 @@ +"""CycleScheduler: orchestrates cycles for multiple underlyings within a master cycle. + +For switches: manages staggered ON/OFF PWM timing to minimize overlap +and smooth electrical load. +For valves: passthrough mode that calls set_valve_open_percent() directly +without temporal scheduling. +""" + +import logging +import time +from vtherm_api.log_collector import get_vtherm_logger +from typing import Any, Callable + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.event import async_call_later + + +from .vtherm_hvac_mode import VThermHvacMode, VThermHvacMode_OFF + +from .cycle_tick_logic import ( + UnderlyingCycleState, + compute_circular_offsets, + compute_target_state, + evaluate_need_on, + evaluate_need_off, + compute_e_eff, +) +_LOGGER = get_vtherm_logger(__name__) + + +def calculate_cycle_times( + on_percent: float, + cycle_min: int, + minimal_activation_delay: int | None = 0, + minimal_deactivation_delay: int | None = 0, +) -> tuple[int, int, bool]: + """Convert on_percent to on_time_sec and off_time_sec. + + Applies minimal activation and deactivation delays to avoid + very short on/off periods that may damage equipment or be ineffective. + + Args: + on_percent: The calculated heating percentage (0.0 to 1.0) + cycle_min: The cycle duration in minutes + minimal_activation_delay: Minimum on time in seconds (below this, turn off) + minimal_deactivation_delay: Minimum off time in seconds (below this, stay on) + + Returns: + Tuple of (on_time_sec, off_time_sec, forced_by_timing) + - forced_by_timing: True if min_on or min_off delays modified the percentage significantly. + """ + min_on = minimal_activation_delay if minimal_activation_delay is not None else 0 + min_off = minimal_deactivation_delay if minimal_deactivation_delay is not None else 0 + + on_percent = max(0.0, min(1.0, on_percent)) + + cycle_sec = cycle_min * 60 + on_time_sec = on_percent * cycle_sec + forced_by_timing = False + + if on_time_sec > 0 and on_time_sec < min_on: + on_time_sec = 0 + forced_by_timing = True + + off_time_sec = cycle_sec - on_time_sec + + if on_time_sec < cycle_sec and off_time_sec < min_off: + on_time_sec = cycle_sec + off_time_sec = 0 + forced_by_timing = True + + return int(on_time_sec), int(off_time_sec), forced_by_timing + + +class CycleScheduler: + """Orchestrates cycles for multiple underlyings within a master cycle. + + For switches: all underlyings operate within the same time window. + ON periods are staggered using computed offsets to minimize overlap. + For valves: passthrough mode — calls set_valve_open_percent() directly. + """ + + def __init__( + self, + hass: HomeAssistant, + thermostat: Any, + underlyings: list, + cycle_duration_sec: float, + min_activation_delay: int = 0, + min_deactivation_delay: int = 0, + ): + self._hass = hass + self._thermostat = thermostat + self._underlyings = underlyings + self._cycle_duration_sec = cycle_duration_sec + self.min_activation_delay: int = min_activation_delay + self.min_deactivation_delay: int = min_deactivation_delay + self._tick_unsub: CALLBACK_TYPE | None = None + self._cycle_end_unsub: CALLBACK_TYPE | None = None + self._on_cycle_start_callbacks: list[Callable] = [] + self._on_cycle_end_callbacks: list[Callable] = [] + # Current cycle parameters (for repeat at cycle end) + self._current_hvac_mode: VThermHvacMode | None = None + self._current_on_time_sec: float = 0 + self._current_off_time_sec: float = 0 + self._current_on_percent: float = 0 + # Active cycle parameters describe what is physically being executed + # right now. They intentionally differ from _current_* when a running + # cycle receives non-forced updates that should only apply to the next + # repeat at cycle end. + self._active_hvac_mode: VThermHvacMode | None = None + self._active_on_time_sec: float = 0 + self._active_off_time_sec: float = 0 + self._active_on_percent: float = 0 + self._states: list[UnderlyingCycleState] = [] + self._penalty: float = 0.0 + self._cycle_start_time: float = 0.0 + # For valves, keep the applied-power segments within the current + # master cycle so realized e_eff reflects real mid-cycle updates. + self._valve_cycle_trace: list[tuple[float, float]] = [] + # Guard flags to keep is_cycle_running=True during async yield points + # where timers are not yet (re-)installed: + # _is_cancelling: True while cancel_cycle() awaits _fire_cycle_end_callbacks() + # _is_starting: True while start_cycle() awaits callbacks or device-control + # operations before the new timers are set + self._is_cancelling: bool = False + self._is_starting: bool = False + # Detect valve mode from underlying types + self._is_valve_mode: bool = self._detect_valve_mode() + + @property + def is_cycle_running(self) -> bool: + """Return True if a cycle is currently scheduled or in a lifecycle transition. + + _is_cancelling stays True while cancel_cycle() awaits _fire_cycle_end_callbacks(), + preventing concurrent start_cycle(force=False) from seeing is_cycle_running=False + and starting a duplicate cycle during that window (Race 1). + + _is_starting stays True while start_cycle() has finished cancellation but has not + yet finished device-control setup (including _fire_cycle_start_callbacks and + _start_cycle_switch/_start_cycle_valve), preventing concurrent start_cycle(force=False) + from starting a duplicate cycle during that window (Race 2). + """ + return self._tick_unsub is not None or self._cycle_end_unsub is not None or self._is_cancelling or self._is_starting + + @property + def is_valve_mode(self) -> bool: + """Return True if managing valve underlyings (passthrough mode).""" + return self._is_valve_mode + + def _detect_valve_mode(self) -> bool: + """Detect if underlyings are valves by checking entity_type.""" + from .underlyings import UnderlyingEntityType # pylint: disable=import-outside-toplevel + if not self._underlyings: + return False + return self._underlyings[0].entity_type in ( + UnderlyingEntityType.VALVE, + UnderlyingEntityType.VALVE_REGULATION, + ) + + def register_cycle_start_callback(self, callback: Callable): + """Register a callback to be called at the start of each master cycle. + + Callback signature: async def callback(on_time_sec, off_time_sec, on_percent, hvac_mode) + """ + self._on_cycle_start_callbacks.append(callback) + + def register_cycle_end_callback(self, callback: Callable[[float], Any]): + """Register a callback to be called at the end of each master cycle.""" + self._on_cycle_end_callbacks.append(callback) + + def _set_pending_cycle( + self, + hvac_mode: VThermHvacMode | None, + on_time_sec: float, + off_time_sec: float, + on_percent: float, + ) -> None: + """Store parameters that the next cycle repeat must use.""" + self._current_hvac_mode = hvac_mode + self._current_on_time_sec = on_time_sec + self._current_off_time_sec = off_time_sec + self._current_on_percent = on_percent + + def _set_active_cycle( + self, + hvac_mode: VThermHvacMode | None, + on_time_sec: float, + off_time_sec: float, + on_percent: float, + ) -> None: + """Store parameters of the master cycle currently being executed.""" + self._active_hvac_mode = hvac_mode + self._active_on_time_sec = on_time_sec + self._active_off_time_sec = off_time_sec + self._active_on_percent = on_percent + + @staticmethod + def _same_cycle_request( + hvac_mode_a: VThermHvacMode | None, + on_time_sec_a: float, + off_time_sec_a: float, + on_percent_a: float, + hvac_mode_b: VThermHvacMode | None, + on_time_sec_b: float, + off_time_sec_b: float, + on_percent_b: float, + ) -> bool: + """Return True when two cycle requests are effectively identical.""" + return ( + hvac_mode_a == hvac_mode_b + and on_time_sec_a == on_time_sec_b + and off_time_sec_a == off_time_sec_b + and abs(on_percent_a - on_percent_b) <= 1e-9 + ) + + async def start_cycle( + self, + hvac_mode: VThermHvacMode, + on_percent: float, + force: bool = False, + _from_cycle_end: bool = False, + ): + """Start a new master cycle for all underlyings. + + Computes on_time_sec and off_time_sec from on_percent, applying + min_activation_delay and min_deactivation_delay constraints. + + Args: + hvac_mode: Current HVAC mode. + on_percent: Power percentage as a fraction (0.0 to 1.0). + force: If True, cancel any running cycle and restart immediately. + _from_cycle_end: Internal flag — True when called from _on_master_cycle_end. + """ + cycle_min = self._cycle_duration_sec / 60 + on_time_sec, off_time_sec, _ = calculate_cycle_times( + on_percent, + cycle_min, + self.min_activation_delay, + self.min_deactivation_delay, + ) + realized_on_percent = on_time_sec / self._cycle_duration_sec if self._cycle_duration_sec > 0 else 0.0 + + # Always update thermostat timing attributes immediately so sensors + # reflect the latest computed value, even when the cycle returns early. + self._thermostat._on_time_sec = on_time_sec + self._thermostat._off_time_sec = off_time_sec + + if self.is_cycle_running and not force: + if self._is_valve_mode: + # Valve mode must keep the current master-cycle window for + # learning callbacks, but the physical valve command still has + # to follow each new regulation result immediately. + _LOGGER.debug( + "%s - Valve cycle already running, applying immediate update: " + "on_time=%.0f, off_time=%.0f, on_percent=%.2f", + self._thermostat, + on_time_sec, + off_time_sec, + realized_on_percent, + ) + await self._update_running_valve_cycle( + hvac_mode, + on_time_sec, + off_time_sec, + realized_on_percent, + ) + return + if self._active_on_time_sec > 0: + # A real cycle is actively running — don't interrupt it. + # Just update stored params so the next auto-repeat uses them. + _LOGGER.debug( + "%s - Cycle already running (on_time=%.0fs), skipping (force=%s). " + "Updating params for next repeat: on_time=%.0f, off_time=%.0f, on_percent=%.2f", + self._thermostat, + self._active_on_time_sec, + force, + on_time_sec, + off_time_sec, + realized_on_percent, + ) + self._set_pending_cycle(hvac_mode, on_time_sec, off_time_sec, realized_on_percent) + return + # Current cycle is idle (on_time=0, device off). + # Keep it running if the requested idle cycle is unchanged; otherwise + # cancel it and allow the new cycle to start immediately. + if self._same_cycle_request( + self._active_hvac_mode, + self._active_on_time_sec, + self._active_off_time_sec, + self._active_on_percent, + hvac_mode, + on_time_sec, + off_time_sec, + realized_on_percent, + ): + _LOGGER.debug( + "%s - Current cycle is idle and unchanged, keeping existing cycle", + self._thermostat, + ) + self._set_pending_cycle(hvac_mode, on_time_sec, off_time_sec, realized_on_percent) + return + _LOGGER.debug( + "%s - Current cycle is idle (on_time=0), replacing with changed cycle", + self._thermostat, + ) + + await self._cancel_cycle_impl() + + # Store current cycle parameters for repeat + self._set_pending_cycle(hvac_mode, on_time_sec, off_time_sec, realized_on_percent) + self._set_active_cycle(hvac_mode, on_time_sec, off_time_sec, realized_on_percent) + + # _is_starting guards the Race window: after _cancel_cycle_impl() cleared all + # timers and flags, but before the new timers are set by _start_cycle_switch() or + # _start_cycle_valve(). During this window, is_cycle_running must return True to + # prevent concurrent start_cycle(force=False) from starting a duplicate full cycle. + self._is_starting = True + try: + # Fire cycle start callbacks with realized percent so learners see actual applied power + await self._fire_cycle_start_callbacks(on_time_sec, off_time_sec, realized_on_percent, hvac_mode) + + if self._is_valve_mode: + await self._start_cycle_valve(hvac_mode) + else: + await self._start_cycle_switch(hvac_mode, on_time_sec, off_time_sec, realized_on_percent) + finally: + self._is_starting = False + + async def apply_valve_update( + self, + hvac_mode: VThermHvacMode, + on_percent: float, + ) -> None: + """Apply a deferred valve recompute without running a full control cycle.""" + if not self._is_valve_mode: + return + + cycle_min = self._cycle_duration_sec / 60 + on_time_sec, off_time_sec, _ = calculate_cycle_times( + on_percent, + cycle_min, + self.min_activation_delay, + self.min_deactivation_delay, + ) + realized_on_percent = on_time_sec / self._cycle_duration_sec if self._cycle_duration_sec > 0 else 0.0 + + self._thermostat._on_time_sec = on_time_sec + self._thermostat._off_time_sec = off_time_sec + self._set_pending_cycle(hvac_mode, on_time_sec, off_time_sec, realized_on_percent) + + if self.is_cycle_running: + await self._update_running_valve_cycle( + hvac_mode, + on_time_sec, + off_time_sec, + realized_on_percent, + ) + return + + self._set_active_cycle(hvac_mode, on_time_sec, off_time_sec, realized_on_percent) + await self._apply_valve_command(hvac_mode, on_time_sec, off_time_sec) + + async def _update_running_valve_cycle( + self, + hvac_mode: VThermHvacMode, + on_time_sec: float, + off_time_sec: float, + on_percent: float, + ) -> None: + """Apply a valve update while preserving the current master-cycle window.""" + self._set_pending_cycle(hvac_mode, on_time_sec, off_time_sec, on_percent) + self._set_active_cycle(hvac_mode, on_time_sec, off_time_sec, on_percent) + await self._apply_valve_command(hvac_mode, on_time_sec, off_time_sec) + self._append_valve_cycle_trace(on_percent) + + async def _apply_valve_command( + self, + hvac_mode: VThermHvacMode, + on_time_sec: float, + off_time_sec: float, + ) -> None: + """Apply the latest valve command to all underlyings immediately.""" + for under in self._underlyings: + under._on_time_sec = on_time_sec + under._off_time_sec = off_time_sec + under._hvac_mode = hvac_mode + await under.set_valve_open_percent() + + def _reset_valve_cycle_trace(self, on_percent: float) -> None: + """Start a new valve trace for the current master cycle.""" + self._valve_cycle_trace = [(0.0, max(0.0, min(1.0, on_percent)))] + + def _append_valve_cycle_trace(self, on_percent: float) -> None: + """Append a new applied-power segment for a running valve cycle.""" + if self._cycle_start_time <= 0: + self._reset_valve_cycle_trace(on_percent) + return + + applied_on_percent = max(0.0, min(1.0, on_percent)) + if self._valve_cycle_trace: + last_offset, last_on_percent = self._valve_cycle_trace[-1] + if abs(last_on_percent - applied_on_percent) <= 1e-9: + return + else: + last_offset = 0.0 + + offset = min( + max(0.0, time.time() - self._cycle_start_time), + self._cycle_duration_sec, + ) + offset = max(offset, last_offset) + self._valve_cycle_trace.append((offset, applied_on_percent)) + + async def _start_cycle_valve(self, hvac_mode: VThermHvacMode): + """Valve passthrough: call set_valve_open_percent() on each underlying. + + Valves don't need temporal ON/OFF scheduling. They just need + their open percentage updated. A master cycle window is still kept so + cycle callbacks remain available to SmartPI in valve-based setups. + """ + self._cycle_start_time = time.time() + await self._apply_valve_command( + hvac_mode, + self._current_on_time_sec, + self._current_off_time_sec, + ) + self._reset_valve_cycle_trace(self._current_on_percent) + self._cycle_end_unsub = async_call_later( + self._hass, + self._cycle_duration_sec, + self._on_master_cycle_end, + ) + + async def _start_cycle_switch( + self, + hvac_mode: VThermHvacMode, + on_time_sec: float, + off_time_sec: float, + on_percent: float, + ): + """Switch True Tick scheduling: initialize cycle and start ticking.""" + # Update on_time/off_time on each underlying for keep-alive and monitoring + for under in self._underlyings: + under._on_time_sec = on_time_sec + under._off_time_sec = off_time_sec + under._hvac_mode = hvac_mode + + if hvac_mode == VThermHvacMode_OFF or on_time_sec <= 0: + # Turn off all underlyings + for under in self._underlyings: + if under.is_device_active: + await under.turn_off() + under._should_be_on = False + # Keep a real master-cycle start time so the next automatic restart + # reports a full elapsed_ratio instead of looking interrupted with 0 s elapsed. + self._cycle_start_time = time.time() + # Schedule next cycle evaluation + self._cycle_end_unsub = async_call_later(self._hass, self._cycle_duration_sec, self._on_master_cycle_end) + return + + if on_time_sec >= self._cycle_duration_sec: + # 100% power: Turn on all underlyings unconditionally to enforce state + for under in self._underlyings: + await under.turn_on() + under._should_be_on = True + # Keep a real master-cycle start time for the same reason as 0% cycles. + self._cycle_start_time = time.time() + # Schedule next cycle evaluation + self._cycle_end_unsub = async_call_later(self._hass, self._cycle_duration_sec, self._on_master_cycle_end) + return + + self._init_cycle(on_percent) + + # Start ticking immediately with is_initial=True to enforce state + await self._tick(_is_initial=True) + + # Also ensure master cycle end is scheduled independently to wrap up the cycle + self._cycle_end_unsub = async_call_later(self._hass, self._cycle_duration_sec, self._on_master_cycle_end) + + def _init_cycle(self, on_percent: float): + """Initialize states and penalty for the new cycle. + + Uses circular offsets for evenly distributed power across underlyings, + with natural wrap-around for smooth load distribution. + """ + self._penalty = 0.0 + self._cycle_start_time = time.time() + + n = len(self._underlyings) + on_time = self._cycle_duration_sec * on_percent + offsets = compute_circular_offsets(self._cycle_duration_sec, n) + + self._states = [] + for i, under in enumerate(self._underlyings): + state = UnderlyingCycleState(under, offsets[i]) + state.on_t = offsets[i] + state.on_time = on_time + state.off_t = (state.on_t + state.on_time) % self._cycle_duration_sec + self._states.append(state) + + _LOGGER.debug( + "%s - Initialized true tick cycle: on_percent=%.2f, offsets=%s", + self._thermostat, on_percent, offsets + ) + + async def _tick(self, _now=None, _is_initial: bool = False): + """Evaluate all underlyings and schedule the next tick. + + When _is_initial=True (called at cycle start), current_t is forced to 0.0 + to avoid floating-point drift, and the is_device_active check is skipped so + the desired state is always enforced unconditionally on the first tick. + """ + from homeassistant.util import dt as dt_util + now = time.time() + current_t = 0.0 if _is_initial else (now - self._cycle_start_time) + + if not _is_initial and current_t >= self._cycle_duration_sec: + # We reached the end of the cycle; let the master cycle end handle it. + return + + next_global_tick = self._cycle_duration_sec - current_t + + for state in self._states: + under = state.underlying + + target_is_on, next_tick, state_duration = compute_target_state( + state.on_t, state.off_t, current_t, self._cycle_duration_sec + ) + + # Update global next tick to the earliest upcoming event + time_to_next = next_tick - current_t + if time_to_next > 0 and time_to_next < next_global_tick: + next_global_tick = time_to_next + + under_dt = 0.0 + if under.last_change: + under_dt = (dt_util.utcnow() - under.last_change).total_seconds() + else: + under_dt = 999999.0 # Safe large value if no history + + if target_is_on: + if not under.is_device_active: + action, new_on_t, pen_delta = evaluate_need_on( + under_dt, state_duration, + self.min_deactivation_delay, self.min_activation_delay, + state.on_t, current_t + ) + if action == 'turn_on': + _LOGGER.info( + "%s - tick turn_on (state_duration=%.1fs, initial=%s)", + under, state_duration, _is_initial + ) + try: + await under.turn_on() + under._should_be_on = True + except Exception as err: + _LOGGER.error("%s - tick turn_on failed: %s", under, err) + elif action == 'skip' and new_on_t is not None: + _LOGGER.debug( + "%s - tick skip turn_on (racollage), on_t shifted %.1f -> %.1f, penalty=%.1f", + under, state.on_t, new_on_t, pen_delta + ) + state.on_t = new_on_t + self._penalty += pen_delta + resched_time = new_on_t - current_t + if 0 < resched_time < next_global_tick: + next_global_tick = resched_time + elif _is_initial: + # Enforce state unconditionally on the first tick + if not under.is_device_active: + await under.turn_on() + under._should_be_on = True + + elif not target_is_on: + if under.is_device_active: + action, new_off_t, pen_delta = evaluate_need_off( + under_dt, state_duration, + self.min_activation_delay, self.min_deactivation_delay, + state.off_t, current_t + ) + if action == 'turn_off': + _LOGGER.info( + "%s - tick turn_off (state_duration=%.1fs, initial=%s)", + under, state_duration, _is_initial + ) + try: + await under.turn_off() + under._should_be_on = False + except Exception as err: + _LOGGER.error("%s - tick turn_off failed: %s", under, err) + elif action == 'skip' and new_off_t is not None: + _LOGGER.debug( + "%s - tick skip turn_off (racollage), off_t shifted %.1f -> %.1f, penalty=%.1f", + under, state.off_t, new_off_t, pen_delta + ) + state.off_t = new_off_t + self._penalty += pen_delta + resched_time = new_off_t - current_t + if 0 < resched_time < next_global_tick: + next_global_tick = resched_time + elif _is_initial: + # Enforce state unconditionally on the first tick + if under.is_device_active: + try: + await under.turn_off() + under._should_be_on = False + except Exception as err: + _LOGGER.error("%s - initial turn_off failed: %s", under, err) + + # Ensure we do not schedule too fast (< 0.1s) + next_global_tick = max(0.1, next_global_tick) + + # Schedule next tick + self._tick_unsub = async_call_later(self._hass, next_global_tick, self._tick) + + async def _on_master_cycle_end(self, _now): + """Called at the end of the master cycle. Restart with the same parameters. + + The cycle end callback (e_eff) is fired by cancel_cycle(), which is called + inside start_cycle(force=True). This ensures exactly one callback per cycle + end regardless of how the cycle terminates. + """ + if not self.is_cycle_running: + return + + # Increment energy counter + self._thermostat.incremente_energy() + + # Restart cycle — cancel_cycle() inside start_cycle will fire the end callback. + await self.start_cycle( + self._current_hvac_mode, + self._current_on_percent, + force=True, + _from_cycle_end=True, + ) + + def shutdown(self): + """Cancel pending timers immediately without firing end-of-cycle callbacks. + + Must be called synchronously when the entity is being removed from HA so + that leftover async_call_later handles cannot fire after the new entity + (potentially with a different cycle duration) has already started. + """ + if self._tick_unsub: + self._tick_unsub() + self._tick_unsub = None + if self._cycle_end_unsub: + self._cycle_end_unsub() + self._cycle_end_unsub = None + self._valve_cycle_trace = [] + self._set_pending_cycle(None, 0, 0, 0.0) + self._set_active_cycle(None, 0, 0, 0.0) + self._is_cancelling = False + self._is_starting = False + + async def cancel_cycle(self): + """Cancel the current cycle if one is running.""" + await self._cancel_cycle_impl() + + async def _cancel_cycle_impl(self): + """Internal cancel logic, shared by cancel_cycle() and start_cycle().""" + was_running = self.is_cycle_running + + # Set before unsubscribing so that is_cycle_running stays True + # throughout this coroutine, even during the async yield below (Race 1 guard). + self._is_cancelling = True + + if self._tick_unsub: + self._tick_unsub() + self._tick_unsub = None + if self._cycle_end_unsub: + self._cycle_end_unsub() + self._cycle_end_unsub = None + + elapsed_sec = time.time() - self._cycle_start_time if self._cycle_start_time > 0 else 0 + + # Fire end-of-cycle callback for cycles that ran long enough. + # This includes normal ends (via _on_master_cycle_end -> start_cycle(force=True)) + # and mid-cycle interruptions (force restart on setpoint change, etc.). + # During this await, _is_cancelling=True keeps is_cycle_running=True so any + # concurrent start_cycle(force=False) call takes the update path, not full start. + if was_running and elapsed_sec > 1.0: + realized_e_eff = self._calculate_realized_e_eff(elapsed_sec) + elapsed_ratio = min(1.0, elapsed_sec / self._cycle_duration_sec) if self._cycle_duration_sec > 0 else 1.0 + + _LOGGER.debug("%s - cycle end: elapsed_sec=%.1f, realized_e_eff=%.3f, elapsed_ratio=%.2f", self._thermostat, elapsed_sec, realized_e_eff, elapsed_ratio) + await self._fire_cycle_end_callbacks(realized_e_eff, elapsed_ratio) + + self._states = [] + self._valve_cycle_trace = [] + self._set_pending_cycle(None, 0, 0, 0.0) + self._set_active_cycle(None, 0, 0, 0.0) + self._cycle_start_time = 0.0 + # Reset only after all state is cleared so the guard stays active until fully done. + self._is_cancelling = False + _LOGGER.debug("%s - Cycle cancelled", self._thermostat) + + def _calculate_realized_e_eff(self, elapsed_sec: float) -> float: + """Calculate the actual effective power applied over the given elapsed time.""" + if not self._underlyings or elapsed_sec <= 0: + return 0.0 + + if self._is_valve_mode: + if not self._valve_cycle_trace: + return max(0.0, min(1.0, self._active_on_percent)) + + weighted_power = 0.0 + for idx, (start_offset, on_percent) in enumerate(self._valve_cycle_trace): + if start_offset >= elapsed_sec: + break + end_offset = elapsed_sec + if idx + 1 < len(self._valve_cycle_trace): + end_offset = min(self._valve_cycle_trace[idx + 1][0], elapsed_sec) + if end_offset > start_offset: + weighted_power += (end_offset - start_offset) * on_percent + + return max(0.0, min(1.0, weighted_power / elapsed_sec)) + + # When _states is empty the cycle ran at either 0% or 100% (no tick scheduling). + # Infer from _active_on_time_sec: if it covers the full duration, e_eff = 1.0. + if not self._states: + if self._active_on_time_sec >= self._cycle_duration_sec: + return 1.0 + return 0.0 + + t_on_actual = 0.0 + for state in self._states: + if state.off_t >= state.on_t: + start_on = min(state.on_t, elapsed_sec) + end_on = min(state.off_t, elapsed_sec) + if end_on > start_on: + t_on_actual += (end_on - start_on) + else: + end_on_1 = min(state.off_t, elapsed_sec) + t_on_actual += end_on_1 + + start_on_2 = min(state.on_t, elapsed_sec) + end_on_2 = elapsed_sec + if end_on_2 > start_on_2: + t_on_actual += (end_on_2 - start_on_2) + + # e_eff is the true instantaneous duty cycle over the elapsed window. + e_eff = max(0.0, t_on_actual - self._penalty) / (elapsed_sec * len(self._underlyings)) + return max(0.0, min(1.0, e_eff)) + + async def _fire_cycle_start_callbacks( + self, on_time_sec, off_time_sec, on_percent, hvac_mode + ): + """Fire all registered cycle start callbacks.""" + for callback in self._on_cycle_start_callbacks: + try: + await callback( + on_time_sec=on_time_sec, + off_time_sec=off_time_sec, + on_percent=on_percent, + hvac_mode=hvac_mode, + ) + except Exception as ex: + _LOGGER.warning( + "%s - Error calling cycle start callback %s: %s", + self._thermostat, + callback, + ex, + ) + + async def _fire_cycle_end_callbacks(self, e_eff: float, elapsed_ratio: float = 1.0): + """Fire all registered cycle end callbacks with e_eff and elapsed_ratio.""" + cycle_duration_min = self._cycle_duration_sec / 60.0 + for callback in self._on_cycle_end_callbacks: + try: + await callback( + e_eff=e_eff, + elapsed_ratio=elapsed_ratio, + cycle_duration_min=cycle_duration_min, + ) + except Exception as ex: + _LOGGER.warning( + "%s - Error calling cycle end callback %s: %s", + self._thermostat, + callback, + ex, + ) diff --git a/custom_components/versatile_thermostat/cycle_tick_logic.py b/custom_components/versatile_thermostat/cycle_tick_logic.py new file mode 100644 index 0000000..bf5ccd8 --- /dev/null +++ b/custom_components/versatile_thermostat/cycle_tick_logic.py @@ -0,0 +1,127 @@ +"""Pure logic for the Versatile Thermostat True Tick Cycle Scheduler.""" + +class UnderlyingCycleState: + """Per-underlying state tracking for a single cycle.""" + + def __init__(self, underlying, offset: float): + """Initialize the state with an underlying reference and a fixed circular offset.""" + self.underlying = underlying + self.offset = offset + self.on_t: float = 0.0 + self.off_t: float = 0.0 + self.on_time: float = 0.0 + + +def compute_circular_offsets(cycle_duration_sec: float, n: int) -> list[float]: + """Compute evenly-spaced circular offsets for n underlyings. + + Returns: + List of start offsets in seconds for each underlying. + """ + if n <= 1: + return [0.0] * n + + step = cycle_duration_sec / n + return [round(i * step, 1) for i in range(n)] + + +def compute_target_state( + on_t: float, off_t: float, current_t: float, cycle_duration: float +) -> tuple[bool, float, float]: + """Determine the theoretical target state and the next tick timestamp. + + Returns: + (target_is_on, next_tick, state_duration) + """ + if off_t > on_t: # ON is confined in [on_t, off_t) + if current_t < on_t: + target = False + next_tick = on_t + elif current_t < off_t: + target = True + next_tick = off_t + else: + target = False + next_tick = cycle_duration + else: # ON wraps around: [0, off_t) U [on_t, cycle_end) + if current_t < off_t: + target = True + next_tick = off_t + elif current_t < on_t: + target = False + next_tick = on_t + else: + target = True + next_tick = cycle_duration + + # Note (off_t == on_t falls in wrap-around condition): + # This works safely because upstream guards in cycle_scheduler._start_cycle_switch + # handle `on_time == 0` and `on_time == cycle_duration` before evaluating ticks. + state_duration = next_tick - current_t + return target, next_tick, state_duration + + +def evaluate_need_on( + under_dt: float, state_duration: float, + min_deactivation: float, min_activation: float, + on_t: float, current_t: float, +) -> tuple[str, float | None, float]: + """Evaluate whether to actually turn ON (need_on) according to constraints. + + Returns: + (action, new_on_t_or_none, penalty_delta) + action is 'turn_on' or 'skip' + """ + if under_dt >= min_deactivation and state_duration > min_activation: + return 'turn_on', None, 0.0 + + # CAS RACOLLAGE (Skip this turn ON) + new_on_t = max(0.0, on_t - state_duration) + penalty_delta = state_duration + + if (new_on_t - current_t) < (min_deactivation - under_dt): + new_on_t = current_t + (min_deactivation - under_dt) + delay = new_on_t - current_t + penalty_delta = delay if delay < state_duration else state_duration + + return 'skip', new_on_t, penalty_delta + + +def evaluate_need_off( + under_dt: float, state_duration: float, + min_activation: float, min_deactivation: float, + off_t: float, current_t: float, +) -> tuple[str, float | None, float]: + """Evaluate whether to actually turn OFF (need_off) according to constraints. + + Returns: + (action, new_off_t_or_none, penalty_delta) + action is 'turn_off' or 'skip' + """ + if under_dt >= min_activation and state_duration > min_deactivation: + return 'turn_off', None, 0.0 + + # CAS RACOLLAGE (Skip this turn OFF) + new_off_t = max(0.0, off_t - state_duration) + penalty_delta = -state_duration + + if (new_off_t - current_t) < (min_activation - under_dt): + new_off_t = current_t + (min_activation - under_dt) + delay = new_off_t - current_t + penalty_delta = -delay if delay < state_duration else -state_duration + + return 'skip', new_off_t, penalty_delta + + +def compute_e_eff( + on_percent: float, penalty: float, + cycle_duration: float, n_underlyings: int, +) -> float: + """Compute effective power ratio (e_eff) at the end of the cycle.""" + if n_underlyings == 0 or cycle_duration <= 0: + return 0.0 + + full_on_t = cycle_duration * n_underlyings + e_eff = (full_on_t * on_percent - penalty) / full_on_t + + return max(0.0, min(1.0, e_eff)) diff --git a/custom_components/versatile_thermostat/ema.py b/custom_components/versatile_thermostat/ema.py new file mode 100644 index 0000000..08a7fb9 --- /dev/null +++ b/custom_components/versatile_thermostat/ema.py @@ -0,0 +1,93 @@ +# pylint: disable=line-too-long +"""The Estimated Mobile Average calculation used for temperature slope +and maybe some others feature""" + +import logging +from vtherm_api.log_collector import get_vtherm_logger +import math +from datetime import datetime, tzinfo + +_LOGGER = get_vtherm_logger(__name__) + +MIN_TIME_DECAY_SEC = 0 + +# MAX_ALPHA: +# As for the EMA calculation of irregular time series, I've seen that it might be useful to +# have an upper limit for alpha in case the last measurement was too long ago. +# For example when using a half life of 10 minutes a measurement that is 60 minutes ago +# (if there's nothing inbetween) would contribute to the smoothed value with 1,5%, +# giving the current measurement 98,5% relevance. It could be wise to limit the alpha to e.g. 4x the half life (=0.9375). + + +class ExponentialMovingAverage: + """A class that will do the Estimated Mobile Average calculation""" + + def __init__( + self, + vterm_name: str, + halflife: float, + timezone: tzinfo, + precision: int = 3, + max_alpha: float = 0.5, + ): + """The halflife is the duration in secondes of a normal cycle""" + self._halflife: float = halflife + self._timezone = timezone + self._current_ema: float = None + self._last_timestamp: datetime = datetime.now(self._timezone) + self._name = vterm_name + self._precision = precision + self._max_alpha = max_alpha + + def __str__(self) -> str: + return f"EMA-{self._name}" + + def calculate_ema(self, measurement: float, timestamp: datetime) -> float | None: + """Calculate the new EMA from a new measurement measured at timestamp + Return the EMA or None if all parameters are not initialized now + """ + + if measurement is None or timestamp is None: + _LOGGER.warning( + "%s - Cannot calculate EMA: measurement and timestamp are mandatory. This message can be normal at startup but should not persist", + self, + ) + return measurement + + if self._current_ema is None: + _LOGGER.debug( + "%s - First init of the EMA", + self, + ) + self._current_ema = measurement + self._last_timestamp = timestamp + return self._current_ema + + time_decay = (timestamp - self._last_timestamp).total_seconds() + if time_decay < MIN_TIME_DECAY_SEC: + _LOGGER.debug( + "%s - time_decay %s is too small (< %s). Forget the measurement", + self, + time_decay, + MIN_TIME_DECAY_SEC, + ) + return self._current_ema + + alpha = 1 - math.exp(math.log(0.5) * time_decay / self._halflife) + # capping alpha to avoid gap if last measurement was long time ago + alpha = min(alpha, self._max_alpha) + new_ema = alpha * measurement + (1 - alpha) * self._current_ema + + self._last_timestamp = timestamp + self._current_ema = new_ema + _LOGGER.debug( + "%s - timestamp=%s alpha=%.2f measurement=%.2f current_ema=%.2f new_ema=%.2f", + self, + timestamp, + alpha, + measurement, + self._current_ema, + new_ema, + ) + + return round(self._current_ema, self._precision) diff --git a/custom_components/versatile_thermostat/feature_auto_start_stop_manager.py b/custom_components/versatile_thermostat/feature_auto_start_stop_manager.py new file mode 100644 index 0000000..2fffd23 --- /dev/null +++ b/custom_components/versatile_thermostat/feature_auto_start_stop_manager.py @@ -0,0 +1,318 @@ +""" Implements the Auto-start/stop Feature Manager """ + +# pylint: disable=line-too-long + +import logging +from datetime import datetime +from vtherm_api.log_collector import get_vtherm_logger +from typing import Any + +from homeassistant.core import ( + HomeAssistant, + callback, +) + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons_type import ConfigData + +from .commons import write_event_log + +from .base_manager import BaseFeatureManager + +from .auto_start_stop_algorithm import ( + AutoStartStopDetectionAlgorithm, +) + + +_LOGGER = get_vtherm_logger(__name__) + + +class FeatureAutoStartStopManager(BaseFeatureManager): + """The implementation of the AutoStartStop feature""" + + unrecorded_attributes = frozenset( + { + "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", + } + ) + + def __init__(self, vtherm: Any, hass: HomeAssistant): + """Init of a featureManager""" + super().__init__(vtherm, hass) + + self._auto_start_stop_level: str = AUTO_START_STOP_LEVEL_NONE + self._auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None + self._is_configured: bool = False + self._is_auto_start_stop_enabled: bool = False + self._is_auto_stop_detected: bool = False + + @overrides + def post_init(self, entry_infos: ConfigData): + """Reinit of the manager""" + + if not self._vtherm.is_over_climate or self._vtherm.have_valve_regulation: + return + + use_auto_start_stop = entry_infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False) + if use_auto_start_stop: + self._auto_start_stop_level = ( + entry_infos.get(CONF_AUTO_START_STOP_LEVEL, None) + or AUTO_START_STOP_LEVEL_NONE + ) + self._is_configured = True + else: + self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE + self._is_configured = False + + # Initialize the enable flag synchronously so the manager is operational + # without having to wait for the AutoStartStopEnable switch's + # async_added_to_hass callback (which is racy under test load). + # The switch's async_added_to_hass will later override this value + # from the persisted state if needed. + # This must mirror the default in switch.AutoStartStopEnable. + self._is_auto_start_stop_enabled = 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 + ) + + # Fix an eventual incoherent state + if self._vtherm.is_on and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP: + self._vtherm.hvac_off_reason = None + + @overrides + async def start_listening(self): + """Start listening the underlying entity""" + + @overrides + def stop_listening(self): + """Stop listening and remove the eventual timer still running""" + + @overrides + async def refresh_state(self) -> bool: + """Check the auto-start-stop and an eventual action + Return True is auto stop is detected""" + + if not self._is_configured or not self._is_auto_start_stop_enabled: + _LOGGER.debug("%s - auto start/stop is disabled (or not configured)", self) + self._is_auto_stop_detected = False + else: + # Do the auto-start-stop calculation + slope = (self._vtherm.last_temperature_slope or 0) / 60 # to have the slope in °/min + should_be_off = self._auto_start_stop_algo.should_be_turned_off( + self._vtherm.requested_state.hvac_mode, + self._vtherm.target_temperature, + self._vtherm.current_temperature, + slope, + self._vtherm.now, + ) + _LOGGER.debug("%s - auto_start_stop should be off is %s", self, should_be_off) + if should_be_off: + _LOGGER.info("%s - VTherm should be OFF due to auto-start-stop conditions", self) + # self._vtherm.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP) + # await self._vtherm.async_turn_off() + + # Send an event if vtherm is on + if not self._is_auto_stop_detected: + self._vtherm.send_event( + event_type=EventType.AUTO_START_STOP_EVENT, + data={ + "type": "stop", + "name": self.name, + "cause": "Auto stop conditions reached", + "hvac_mode": str(VThermHvacMode_OFF), + "saved_hvac_mode": str(self._vtherm.requested_state.hvac_mode), + "target_temperature": self._vtherm.target_temperature, + "current_temperature": self._vtherm.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._is_auto_stop_detected = True + + else: + _LOGGER.info("%s - VTherm should be ON due to auto-start-stop conditions", self) + + # await self._vtherm.async_turn_on() + + # Send an event + if self._is_auto_stop_detected: + self._vtherm.send_event( + event_type=EventType.AUTO_START_STOP_EVENT, + data={ + "type": "start", + "name": self.name, + "cause": "Auto start conditions reached", + "hvac_mode": str(self._vtherm.requested_state.hvac_mode), + "saved_hvac_mode": str(self._vtherm.requested_state.hvac_mode), + "target_temperature": self._vtherm.target_temperature, + "current_temperature": self._vtherm.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._is_auto_stop_detected = False + + # returns True if we should stop + return self._is_auto_stop_detected + + async def refresh_and_update_if_changed(self) -> bool: + """Refresh the auto start/stop state and update_states of VTherm if changed + Returns True if the state has changed, False otherwise""" + old_auto_start_stop: bool = self.is_auto_stop_detected + if old_auto_start_stop != await self.refresh_state(): + write_event_log(_LOGGER, self._vtherm, f"Auto start/stop state changed from {old_auto_start_stop} to {self.is_auto_stop_detected}") + self._vtherm.requested_state.force_changed() + await self._vtherm.update_states(force=True) + return True + + return False + + async def set_auto_start_stop_enable(self, is_enabled: bool): + """Enable/Disable the auto-start/stop feature""" + if self._is_auto_start_stop_enabled != is_enabled: + self._is_auto_start_stop_enabled = is_enabled + + # Send an event if the vtherm was off due to auto-start/stop and enable has been set to false + if not is_enabled and self._vtherm.hvac_mode == VThermHvacMode_OFF and self._vtherm.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") + # Send an event + self._vtherm.send_event( + event_type=EventType.AUTO_START_STOP_EVENT, + data={ + "type": "start", + "name": self.name, + "cause": "Auto start stop disabled", + "hvac_mode": str(self._vtherm.requested_state.hvac_mode), + "saved_hvac_mode": str(self._vtherm.requested_state.hvac_mode), + "target_temperature": self._vtherm.target_temperature, + "current_temperature": self._vtherm.current_temperature, + "temperature_slope": round(self._vtherm.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, + }, + ) + + await self.refresh_state() + self._vtherm.requested_state.force_changed() + await self._vtherm.update_states(True) + + self._vtherm.update_custom_attributes() + + @callback + @overrides + def restore_state(self, old_state) -> None: + """Restore the auto start/stop manager state after a Home Assistant restart. + + Restores: + - _is_auto_stop_detected: so the VTherm stays off if it was auto-stopped + - algo._accumulated_error: so the algorithm continues from where it left off + - algo._last_switch_date: to prevent rapid ON/OFF switching after restart + - algo._last_should_be_off: kept in sync with _is_auto_stop_detected + """ + if old_state is None: + return + + manager_attr = old_state.attributes.get("auto_start_stop_manager") + if not manager_attr: + return + + # Restore the manager-level detected state + self._is_auto_stop_detected = bool(manager_attr.get("is_auto_stop_detected", False)) + + # Restore algorithm intermediate state + if self._auto_start_stop_algo is not None: + accumulated_error = manager_attr.get("auto_start_stop_accumulated_error") + if accumulated_error is not None: + self._auto_start_stop_algo._accumulated_error = float(accumulated_error) + + last_switch_date_raw = manager_attr.get("auto_start_stop_last_switch_date") + if last_switch_date_raw is not None: + if isinstance(last_switch_date_raw, str): + try: + self._auto_start_stop_algo._last_switch_date = datetime.fromisoformat(last_switch_date_raw) + except (ValueError, TypeError) as err: + _LOGGER.warning("%s - restore_state: could not parse last_switch_date '%s': %s", self, last_switch_date_raw, err) + else: + self._auto_start_stop_algo._last_switch_date = last_switch_date_raw + + # Keep algo's internal flag in sync with the manager state + self._auto_start_stop_algo._last_should_be_off = self._is_auto_stop_detected + + _LOGGER.info( + "%s - restore_state: is_auto_stop_detected=%s, accumulated_error=%s", + self, + self._is_auto_stop_detected, + manager_attr.get("auto_start_stop_accumulated_error"), + ) + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): + """Add some custom attributes""" + extra_state_attributes.update( + { + "is_auto_start_stop_configured": self.is_configured, + } + ) + if self.is_configured: + extra_state_attributes.update( + { + "auto_start_stop_manager": { + "auto_start_stop_enable": self.auto_start_stop_enable, + "auto_start_stop_level": self._auto_start_stop_algo.level, + "auto_start_stop_dtmin": self._auto_start_stop_algo.dt_min, + "auto_start_stop_accumulated_error": self._auto_start_stop_algo.accumulated_error, + "auto_start_stop_accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold, + "auto_start_stop_last_switch_date": self._auto_start_stop_algo.last_switch_date, + "is_auto_stop_detected": self.is_auto_stop_detected, + } + } + ) + + @property + def is_auto_stop_detected(self) -> bool: + """Return True if the auto-start/stop feature is detected""" + return self._is_auto_stop_detected + + @property + def is_detected(self) -> bool: + """Return True if the auto-start/stop feature is detected""" + return self._is_auto_stop_detected + + @overrides + @property + def is_configured(self) -> bool: + """Return True of the aiuto-start/stop feature is configured""" + return self._is_configured + + @property + def auto_start_stop_level(self) -> str: + """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 is_auto_stopped(self) -> bool: + """Returns the is vtherm is stopped and reason is AUTO_START_STOP""" + return self._vtherm.hvac_mode == VThermHvacMode_OFF and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP + + def reset_switch_delay(self): + """Reset the switch delay in the algorithm to allow immediate restart. + Should be called when target temperature changes significantly. + """ + if self._auto_start_stop_algo: + self._auto_start_stop_algo.reset_switch_delay() + + def __str__(self): + return f"AutoStartStopManager-{self.name}" diff --git a/custom_components/versatile_thermostat/feature_central_boiler_manager.py b/custom_components/versatile_thermostat/feature_central_boiler_manager.py new file mode 100644 index 0000000..d73fef9 --- /dev/null +++ b/custom_components/versatile_thermostat/feature_central_boiler_manager.py @@ -0,0 +1,468 @@ +"""This module manages the central boiler feature of the Versatile Thermostat integration.""" + +from datetime import timedelta +from typing import Any + +from homeassistant.core import ( + HomeAssistant, + HomeAssistantError, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import ( + async_call_later, + async_track_state_change_event, + async_track_time_interval, +) + +from .base_manager import BaseFeatureManager +from .commons import check_and_extract_service_configuration, write_event_log +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from vtherm_api.log_collector import get_vtherm_logger + + +_LOGGER = get_vtherm_logger(__name__) + + +class FeatureCentralBoilerManager(BaseFeatureManager): + """The implementation of the Central Boiler Feature Manager for Versatile Thermostat""" + + def __init__(self, hass: HomeAssistant, vtherm_api: Any): + """Initialize the FeatureCentralBoilerManager.""" + super().__init__(vtherm_api, hass) + self._vtherm_api = vtherm_api + self._is_configured: bool = False + self._is_ready: bool = False + self._is_on: bool | None = None + self._service_activate: dict | None = None + self._service_deactivate: dict | None = None + + self._central_boiler_entity = None + self._nb_active_device_threshold_number_entity = None + self._total_power_active_threshold_number_entity = None + self._nb_active_device_number_entity = None + self._total_power_active_entity = None + self._all_boiler_entities: list[Entity] = [] + + self._activation_delay_sec: int = 0 + self._call_later_handle = None + + # Keep-alive tracking + self._keep_alive_boiler_state_enabled: bool = False + self._keep_alive_boiler_delay_sec: int = 0 + self._time_interval_listener_cancel = None + self._last_activated_service: dict | None = None + + @overrides + def post_init(self, entry_infos: dict): + """Reinit of the manager""" + self._service_activate = check_and_extract_service_configuration( + entry_infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV) + ) + self._service_deactivate = check_and_extract_service_configuration( + entry_infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV) + ) + self._activation_delay_sec = entry_infos.get(CONF_CENTRAL_BOILER_ACTIVATION_DELAY_SEC, 0) + self._keep_alive_boiler_delay_sec = entry_infos.get(CONF_KEEP_ALIVE_BOILER_DELAY_SEC, DEFAULT_KEEP_ALIVE_BOILER_DELAY_SEC) + self._last_activated_service = None + self._is_configured = bool(self._service_activate or self._service_deactivate) + self._keep_alive_boiler_state_enabled = self._keep_alive_boiler_delay_sec > 0 and self._is_configured + + @overrides + async def start_listening(self, force: bool = False): + """Initialize the listening of state change of VTherms""" + + # Listen to all VTherm state change + boiler_entity_ids = self._get_all_boiler_entity_ids(force=force) + if self.is_ready: + self.stop_listening() + listener_cancel = async_track_state_change_event( + self._hass, + boiler_entity_ids, + self.calculate_central_boiler_state, + ) + _LOGGER.debug( + "%s - entities to get the nb of active VTherm are %s", + self, + boiler_entity_ids, + ) + self.add_listener(listener_cancel) + + # Add periodic keep-alive if delay is configured + if self._keep_alive_boiler_state_enabled: + interval_listener_cancel = async_track_time_interval( + self._hass, + self._keep_alive_boiler, + timedelta(seconds=self._keep_alive_boiler_delay_sec), + ) + self._time_interval_listener_cancel = interval_listener_cancel + self.add_listener(interval_listener_cancel) + _LOGGER.debug( + "%s - Keep-alive boiler enabled (every %d seconds)", + self, + self._keep_alive_boiler_delay_sec, + ) + + await self.calculate_central_boiler_state(None) + else: + _LOGGER.debug("%s - no VTherm could controls the central boiler", self) + + @property + def is_configured(self) -> bool: + """True if the FeatureManager is fully configured""" + return self._is_configured + + @property + def is_ready(self) -> bool: + """True if the FeatureManager is fully configured and has all registered entities""" + return self._is_ready + + @property + def is_on(self) -> bool | None: + """Return True if the central boiler is on. Return None if the state is unknown (at startup for example)""" + return self._is_on + + @property + def is_detected(self) -> bool: + """Return True if the central boiler is detected""" + return self._is_on if self._is_on is not None else False + + async def calculate_central_boiler_state(self, _): + """Calculate the central boiler state depending on all VTherm that + controls this central boiler""" + + _LOGGER.debug("%s - calculating the new central boiler state", self) + + def _send_vtherm_event(data: dict): + send_vtherm_event( + hass=self._hass, + event_type=EventType.CENTRAL_BOILER_EVENT, + entity=self.central_boiler_entity, + data=data, + ) + + async def _activate_later(_): + """Activate the central boiler after a delay""" + if self._call_later_handle: + self._call_later_handle() + self._call_later_handle = None + + # the condition may have changed during the delay, so we check it again before activating the boiler + active = self.is_nb_active_active_for_boiler_exceeded or self.is_total_power_active_for_boiler_exceeded + if not active: + _LOGGER.info("%s - central boiler delayed activation cancelled because condition is not valid anymore", self) + return + + _LOGGER.info("%s - central boiler will be turned on", self) + try: + await self.call_service(self._service_activate) + _LOGGER.info("%s - central boiler have been turned on after delay", self) + self._is_on = True + if self._keep_alive_boiler_state_enabled: + self._last_activated_service = self._service_activate + _send_vtherm_event( + data={"central_boiler": True}, + ) + except HomeAssistantError as err: + _LOGGER.error( + "%s - Impossible to activate boiler due to error %s. " + "Central boiler will not being controlled by VTherm. " + "Please check your service configuration. Cf. README.", + self, + err, + ) + return + + if not self.is_ready: + _LOGGER.warning( + "%s - the central boiler manager is not ready. Central boiler state cannot be calculated", + self, + ) + return False + + active = self.is_nb_active_active_for_boiler_exceeded or self.is_total_power_active_for_boiler_exceeded + + if self._is_on != active: + try: + if active: + write_event_log( + _LOGGER, + self, + f"Central boiler is being turned on (nb_active= {self.nb_active_device_for_boiler}/{self.nb_active_device_for_boiler_threshold}," + "total_power= {self.total_power_active_for_boiler}/{self.total_power_active_for_boiler_threshold})", + ) + if self._call_later_handle: + _LOGGER.debug("%s - central boiler activation is already scheduled", self) + return + + if self._activation_delay_sec > 0: + self._call_later_handle = async_call_later(self._hass, timedelta(seconds=self._activation_delay_sec), _activate_later) + _send_vtherm_event( + data={"delayed_activation_sec": self._activation_delay_sec}, + ) + else: + await _activate_later(None) + else: + write_event_log( + _LOGGER, + self, + f"Central boiler is being turned off (nb_active= {self.nb_active_device_for_boiler}/{self.nb_active_device_for_boiler_threshold}," + "total_power= {self.total_power_active_for_boiler}/{self.total_power_active_for_boiler_threshold})", + ) + + # Cancel any pending activation + if self._call_later_handle: + self._call_later_handle() + self._call_later_handle = None + + # call deactivation service + await self.call_service(self._service_deactivate) + _LOGGER.info("%s - central boiler have been turned off", self) + self._is_on = active + if self._keep_alive_boiler_state_enabled: + self._last_activated_service = self._service_deactivate + _send_vtherm_event( + data={"central_boiler": active}, + ) + except HomeAssistantError as err: + _LOGGER.error( + "%s - Impossible to activate/deactivate boiler due to error %s. " + "Central boiler will not being controlled by VTherm. " + "Please check your service configuration. Cf. README.", + self, + err, + ) + + async def _keep_alive_boiler(self, _=None): + """Send keep-alive command to maintain boiler state (called periodically if delay is configured)""" + if self._keep_alive_boiler_delay_sec <= 0: + return + + # If we have a pending activation (after delay), don't send keep-alive yet + if self._call_later_handle is not None: + return + + # Send keep-alive command for the last activated service + if self._last_activated_service: + try: + await self.call_service(self._last_activated_service) + entity_id = self._last_activated_service.get("entity_id", "unknown") + _LOGGER.debug( + "%s - Keep-alive boiler command sent (entity %s)", + self, + entity_id, + ) + except HomeAssistantError as err: + _LOGGER.warning( + "%s - Failed to send keep-alive command: %s", + self, + err, + ) + + async def call_service(self, service_config: dict): + """Make a call to a service if correctly configured""" + if not service_config: + return + + _LOGGER.debug( + "%s - Calling central boiler service %s with data %s", + self, + f"{service_config['service_domain']}.{service_config['service_name']}", + service_config.get("data", {}), + ) + await self._hass.services.async_call( + service_config["service_domain"], + service_config["service_name"], + service_data=service_config["data"], + target={ + "entity_id": service_config["entity_id"], + }, + ) + + async def reload_central_boiler_binary_listener(self): + """Reloads the BinarySensor entity which listen to the number of + active devices and the thresholds entities""" + if self._nb_active_device_number_entity: + await self._nb_active_device_number_entity.listen_vtherms_entities() + if self._total_power_active_entity: + await self._total_power_active_entity.listen_vtherms_entities() + + async def reload_central_boiler_entities_list(self): + """Reload the central boiler list of entities if a central boiler is used""" + if self._nb_active_device_number_entity is not None: + await self._nb_active_device_number_entity.listen_vtherms_entities() + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]) -> None: + """Add custom attributes to the attributes dict.""" + extra_state_attributes.update( + { + "is_central_boiler_configured": self._is_configured, + "is_central_boiler_ready": self._is_ready, + } + ) + if self._is_ready: + extra_state_attributes.update( + { + "central_boiler_manager": { + "is_on": self._is_on, + "activation_scheduled": self._call_later_handle is not None, + "delayed_activation_sec": self._activation_delay_sec, + "nb_active_device_for_boiler": self.nb_active_device_for_boiler, + "nb_active_device_for_boiler_threshold": self.nb_active_device_for_boiler_threshold, + "total_power_active_for_boiler": self.total_power_active_for_boiler, + "total_power_active_for_boiler_threshold": self.total_power_active_for_boiler_threshold, + "service_activate": self._service_activate, + "service_deactivate": self._service_deactivate, + "keep_alive_boiler_delay_sec": self._keep_alive_boiler_delay_sec, + } + } + ) + + 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""" + self._central_boiler_entity = central_boiler_entity + self.hass.create_task(self.start_listening(force=True)) + + def register_central_boiler_activation_number_threshold(self, threshold_number_entity): + """register the number entities needed for boiler activation""" + self._nb_active_device_threshold_number_entity = threshold_number_entity + self.hass.create_task(self.start_listening(force=True)) + + def register_central_boiler_power_activation_threshold(self, power_threshold_number_entity): + """register the power entities needed for boiler activation""" + self._total_power_active_threshold_number_entity = power_threshold_number_entity + self.hass.create_task(self.start_listening(force=True)) + + def register_nb_device_active_boiler(self, nb_active_number_entity): + """register the two number entities needed for boiler activation""" + self._nb_active_device_number_entity = nb_active_number_entity + self.hass.create_task(self.start_listening(force=True)) + + def register_total_power_active_boiler(self, total_power_active_entity): + """register the two number entities needed for boiler activation""" + self._total_power_active_entity = total_power_active_entity + self.hass.create_task(self.start_listening(force=True)) + + @property + def central_boiler_entity(self): + """Get the central boiler binary_sensor entity""" + return self._central_boiler_entity + + @property + def nb_active_device_for_boiler(self): + """Returns the number of active VTherm which have an + influence on boiler""" + if self._nb_active_device_number_entity is None: + return None + else: + return self._nb_active_device_number_entity.native_value + + @property + def nb_active_device_for_boiler_threshold(self): + """Returns the number of active VTherm entity which have an + influence on boiler""" + if self._nb_active_device_threshold_number_entity is None: + return None + return int(self._nb_active_device_threshold_number_entity.native_value) + + @property + def total_power_active_for_boiler(self): + """Returns the total power of active VTherm which have an + influence on boiler""" + if self._total_power_active_entity is None: + return None + else: + return self._total_power_active_entity.native_value + + @property + def total_power_active_for_boiler_threshold(self): + """Returns the number of active VTherm entity which have an + influence on boiler""" + if self._total_power_active_threshold_number_entity is None: + return None + return int(self._total_power_active_threshold_number_entity.native_value) + + @property + def is_nb_active_active_for_boiler_exceeded(self) -> bool: + """Returns True if the number of active VTherm for boiler + have exceeded the threshold""" + if self.nb_active_device_for_boiler is None or self.nb_active_device_for_boiler_threshold is None or self.nb_active_device_for_boiler_threshold == 0: + return False + + return self.nb_active_device_for_boiler >= self.nb_active_device_for_boiler_threshold + + @property + def is_total_power_active_for_boiler_exceeded(self) -> bool: + """Returns True if the total power of active VTherm for boiler + have exceeded the threshold""" + if self.total_power_active_for_boiler is None or self.total_power_active_for_boiler_threshold is None or self.total_power_active_for_boiler_threshold == 0: + return False + + total_power = self.total_power_active_for_boiler + power_threshold = self.total_power_active_for_boiler_threshold + + return total_power >= power_threshold + + def _get_all_boiler_entity_ids(self, force=False) -> list[str]: + """Returns the list of all VTherm entity ids which have an influence + on the central boiler""" + if self._is_configured and not force: + return self._all_boiler_entities + + self._all_boiler_entities = [] + if self._nb_active_device_threshold_number_entity: + self._all_boiler_entities.append(self._nb_active_device_threshold_number_entity.entity_id) + if self._total_power_active_threshold_number_entity: + self._all_boiler_entities.append(self._total_power_active_threshold_number_entity.entity_id) + if self._nb_active_device_number_entity: + self._all_boiler_entities.append(self._nb_active_device_number_entity.entity_id) + if self._total_power_active_entity: + self._all_boiler_entities.append(self._total_power_active_entity.entity_id) + + old_ready = self._is_ready + self._is_ready = self.is_configured and len(self._all_boiler_entities) == 4 and self._central_boiler_entity is not None + if not self._is_ready and self.is_configured: + _LOGGER.warning( + "%s - central boiler manager is not fully configured. Found only %d/4 entities and central boiler entity=%s. Central boiler control will not work properly. This could a temporary message at startup.", + self, + len(self._all_boiler_entities), + self._central_boiler_entity, + ) + return [] + if not self._is_ready and not self.is_configured: + # Silence warning if the feature is not configured (user doesn't want it) + _LOGGER.debug( + "%s - central boiler manager is not configured. Ignoring initialization.", + self, + ) + return [] + if self._is_ready != old_ready and self._central_boiler_entity: + # Notify the central boiler entity that the manager is now ready + self.hass.create_task(self.refresh_central_boiler_custom_attributes()) + + return self._all_boiler_entities + + async def refresh_central_boiler_custom_attributes(self): + """Refresh the custom attributes of the central boiler entity""" + if self._central_boiler_entity: + self._central_boiler_entity.refresh_custom_attributes() + + @property + def nb_device_active_for_boiler_entity(self): + """Returns the entity if the sensor which gives the number of active VTherm which have an + influence on boiler""" + return self._nb_active_device_number_entity + + # For testing purpose + def _set_nb_active_device_threshold(self, value: int) -> None: + """Set the number of active device threshold""" + if self._nb_active_device_threshold_number_entity: + self._nb_active_device_threshold_number_entity.set_native_value(value) + + def _set_total_power_active_threshold(self, value: int) -> None: + """Set the total power of active device threshold""" + if self._total_power_active_threshold_number_entity: + self._total_power_active_threshold_number_entity.set_native_value(value) + + def __str__(self): + return "FeatureCentralBoilerManager" diff --git a/custom_components/versatile_thermostat/feature_central_power_manager.py b/custom_components/versatile_thermostat/feature_central_power_manager.py new file mode 100644 index 0000000..ab77c34 --- /dev/null +++ b/custom_components/versatile_thermostat/feature_central_power_manager.py @@ -0,0 +1,362 @@ +""" Implements a central Power Feature Manager for Versatile Thermostat """ + +import logging +from vtherm_api.log_collector import get_vtherm_logger + +from typing import Any +from functools import cmp_to_key + +from datetime import timedelta + +from homeassistant.const import STATE_OFF +from homeassistant.core import HomeAssistant, Event, callback +from homeassistant.helpers.event import ( + async_track_state_change_event, + EventStateChangedData, + async_call_later, +) +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.components.climate import ( + ClimateEntity, + DOMAIN as CLIMATE_DOMAIN, +) + + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons import write_event_log +from .commons_type import ConfigData +from .base_manager import BaseFeatureManager + +# circular dependency +# from .base_thermostat import BaseThermostat + +MIN_DTEMP_SECS = 20 + +_LOGGER = get_vtherm_logger(__name__) + + +class FeatureCentralPowerManager(BaseFeatureManager): + """A central Power feature manager""" + + def __init__(self, hass: HomeAssistant, vtherm_api: Any): + """Init of a featureManager""" + super().__init__(None, hass, "centralPowerManager") + self._hass: HomeAssistant = hass + self._vtherm_api = vtherm_api # no type due to circular reference + self._is_configured: bool = False + self._power_sensor_entity_id: str | None = None + self._max_power_sensor_entity_id: str | None = None + self._current_power: float | None = None + self._current_max_power: float | None = None + self._power_temp: float | None = None + self._cancel_calculate_shedding_call = None + self._started_vtherm_total_power_by_id: dict[str, float] = {} + # Not used now + self._last_shedding_date = None + self._state = False + + def post_init(self, entry_infos: ConfigData): + """Gets the configuration parameters""" + central_config = self._vtherm_api.find_central_configuration() + if not central_config: + _LOGGER.info("%s - No central configuration is found. Power management will be deactivated", self) + return + + self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR) + self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR) + self._power_temp = entry_infos.get(CONF_PRESET_POWER) + + self._is_configured = False + self._current_power = None + self._current_max_power = None + if ( + entry_infos.get(CONF_USE_POWER_FEATURE, False) + and self._max_power_sensor_entity_id + and self._power_sensor_entity_id + and self._power_temp + ): + self._is_configured = True + self._started_vtherm_total_power_by_id = {} + else: + _LOGGER.info("%s - Power management is not fully configured and will be deactivated", self) + + async def start_listening(self): + """Start listening the power sensor""" + if not self._is_configured: + return + + self.stop_listening() + + self.add_listener( + async_track_state_change_event( + self.hass, + [self._power_sensor_entity_id], + self._power_sensor_changed, + ) + ) + + self.add_listener( + async_track_state_change_event( + self.hass, + [self._max_power_sensor_entity_id], + self._max_power_sensor_changed, + ) + ) + + @callback + async def _power_sensor_changed(self, event: Event[EventStateChangedData]): + """Handle power changes.""" + write_event_log(_LOGGER, self, f"Receive power sensor state {event.data.get('new_state').state if event.data.get('new_state') else None}") + + self._started_vtherm_total_power_by_id = {} + await self.refresh_state() + + @callback + async def _max_power_sensor_changed(self, event: Event[EventStateChangedData]): + """Handle power max changes.""" + write_event_log(_LOGGER, self, f"Receive max power sensor state {event.data.get('new_state').state if event.data.get('new_state') else None}") + await self.refresh_state() + + @overrides + async def refresh_state(self) -> bool: + """Tries to get the last state from sensor + Returns True if a change has been made""" + + async def _calculate_shedding_internal(_): + _LOGGER.debug("%s - Do the shedding calculation", self) + await self.calculate_shedding() + if self._cancel_calculate_shedding_call: + self._cancel_calculate_shedding_call() + self._cancel_calculate_shedding_call = None + + if not self._is_configured: + return False + + # Retrieve current power + new_power = get_safe_float(self._hass, self._power_sensor_entity_id) + power_changed = new_power is not None and self._current_power != new_power + if power_changed: + self._current_power = new_power + _LOGGER.debug("%s - New current power has been retrieved: %.3f", self, self._current_power) + + # Retrieve max power + new_max_power = get_safe_float(self._hass, self._max_power_sensor_entity_id) + max_power_changed = new_max_power is not None and self._current_max_power != new_max_power + if max_power_changed: + self._current_max_power = new_max_power + _LOGGER.debug("%s - New current max power has been retrieved: %.3f", self, self._current_max_power) + + # Schedule shedding calculation if there's any change + if power_changed or max_power_changed: + if not self._cancel_calculate_shedding_call: + self._cancel_calculate_shedding_call = async_call_later(self.hass, timedelta(seconds=MIN_DTEMP_SECS), _calculate_shedding_internal) + return True + + return False + + # For testing purpose only, do an immediate shedding calculation + async def _do_immediate_shedding(self): + """Do an immmediate shedding calculation if a timer was programmed. + Else, do nothing""" + if self._cancel_calculate_shedding_call: + self._cancel_calculate_shedding_call() + self._cancel_calculate_shedding_call = None + await self.calculate_shedding() + + async def calculate_shedding(self): + """Do the shedding calculation and set/unset VTherm into overpowering state""" + if not self.is_configured or self.current_max_power is None or self.current_power is None: + return + + changed_vtherm = [] + + _LOGGER.debug("%s - -------- Start of calculate_shedding", self) + # Find all VTherms + available_power = self.current_max_power - self.current_power + vtherms_sorted = self.find_all_vtherm_with_power_management_sorted_by_dtemp() + + # shedding only + if available_power < 0: + _LOGGER.debug( + "%s - The available power is is < 0 (%s). Set overpowering only for list: %s", + self, + available_power, + vtherms_sorted, + ) + # we will set overpowering for the nearest target temp first + total_power_gain = 0 + + for vtherm in vtherms_sorted: + if vtherm.is_device_active and not vtherm.power_manager.is_overpowering_detected: + device_power = vtherm.power_manager.device_power + total_power_gain += device_power + _LOGGER.info("vtherm %s should be in overpowering state (device_power=%.2f)", vtherm.name, device_power) + await vtherm.power_manager.set_overpowering(True, device_power) + changed_vtherm.append(vtherm) + + _LOGGER.debug("%s - after vtherm %s total_power_gain=%s, available_power=%s", self, vtherm.name, total_power_gain, available_power) + if total_power_gain >= -available_power: + _LOGGER.debug("%s - We have found enough vtherm to set to overpowering", self) + break + # unshedding only + else: + vtherms_sorted.reverse() + _LOGGER.debug("%s - The available power is is > 0 (%s). Do a complete shedding/un-shedding calculation for list: %s", self, available_power, vtherms_sorted) + + total_power_added = 0 + + for vtherm in vtherms_sorted: + # We want to do always unshedding in order to initialize the state + # so we cannot use is_overpowering_detected which test also UNKNOWN and UNAVAILABLE + if vtherm.power_manager.overpowering_state == STATE_OFF: + continue + + power_consumption_max = device_power = vtherm.power_manager.device_power + # calculate the power_consumption_max + if vtherm.on_percent is not None: + power_consumption_max = max( + device_power / vtherm.nb_underlying_entities, + device_power * vtherm.on_percent, + ) + + _LOGGER.debug( + "%s - vtherm %s power_consumption_max is %s (device_power=%s, overclimate=%s)", self, vtherm.name, power_consumption_max, device_power, vtherm.is_over_climate + ) + + # or not ... is for initializing the overpowering state if not already done + if total_power_added + power_consumption_max < available_power or not vtherm.power_manager.is_overpowering_detected: + # we count the unshedding only if the VTherm was in shedding + if vtherm.power_manager.is_overpowering_detected: + _LOGGER.info("%s - vtherm %s should not be in overpowering state (power_consumption_max=%.2f)", self, vtherm.name, power_consumption_max) + total_power_added += power_consumption_max + + await vtherm.power_manager.set_overpowering(False) + changed_vtherm.append(vtherm) + + if total_power_added >= available_power: + _LOGGER.debug("%s - We have found enough vtherm to set to non-overpowering", self) + break + + _LOGGER.debug("%s - after vtherm %s total_power_added=%s, available_power=%s", self, vtherm.name, total_power_added, available_power) + + # We have set the evenual new state, fr + for vtherm in changed_vtherm: + vtherm.requested_state.force_changed() + await vtherm.update_states(force=True) + self._last_shedding_date = self._vtherm_api.now + + # calculate a state as true if one of the VTherm is in shedding + self._state = any(vtherm.power_manager.is_overpowering_detected for vtherm in vtherms_sorted) + _LOGGER.debug("%s - -------- End of calculate_shedding", self) + + def get_climate_components_entities(self) -> list: + """Get all VTherms entitites""" + vtherms = [] + component: EntityComponent[ClimateEntity] = self._hass.data.get( + CLIMATE_DOMAIN, None + ) + if component: + for entity in list(component.entities): + # 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 + ): + vtherms.append(entity) + return vtherms + + def find_all_vtherm_with_power_management_sorted_by_dtemp( + self, + ) -> list: + """Returns all the VTherms with power management activated""" + entities = self.get_climate_components_entities() + vtherms = [ + vtherm + for vtherm in entities + if vtherm.power_manager.is_configured and vtherm.is_on + ] + + # sort the result with the min temp difference first. A and B should be BaseThermostat class + def cmp_temps(a, b) -> int: + diff_a = float("inf") + diff_b = float("inf") + a_target = a.target_temperature if not a.power_manager.is_overpowering_detected else a.requested_state.target_temperature + b_target = b.target_temperature if not b.power_manager.is_overpowering_detected else b.requested_state.target_temperature + if a.current_temperature is not None and a_target is not None: + diff_a = a_target - a.current_temperature + if b.current_temperature is not None and b_target is not None: + diff_b = b_target - b.current_temperature + + if diff_a == diff_b: + return 0 + return 1 if diff_a > diff_b else -1 + + vtherms.sort(key=cmp_to_key(cmp_temps)) + return vtherms + + def get_started_vtherm_power(self, reservation_key: str) -> float: + """Return the reserved started power for a given underlying key.""" + return self._started_vtherm_total_power_by_id.get(reservation_key, 0.0) + + def set_started_vtherm_power(self, reservation_key: str, started_power: float): + """Set the temporary reserved power for a given underlying key. + + This reservation is used between two power sensor measurements to avoid + allowing several underlyings to start simultaneously based on the same + stale sensor reading. + """ + if started_power > 0: + self._started_vtherm_total_power_by_id[reservation_key] = started_power + else: + self._started_vtherm_total_power_by_id.pop(reservation_key, None) + + _LOGGER.debug( + "%s - started_vtherm_total_power is now %s (%s)", + self, + self.started_vtherm_total_power, + self._started_vtherm_total_power_by_id, + ) + + @property + def is_configured(self) -> bool: + """True if the FeatureManager is fully configured""" + return self._is_configured + + @property + def current_power(self) -> float | None: + """Return the current power from sensor""" + return self._current_power + + @property + def current_max_power(self) -> float | None: + """Return the current power from sensor""" + return self._current_max_power + + @property + def power_temperature(self) -> float | None: + """Return the power temperature""" + return self._power_temp + + @property + def power_sensor_entity_id(self) -> float | None: + """Return the power sensor entity id""" + return self._power_sensor_entity_id + + @property + def max_power_sensor_entity_id(self) -> float | None: + """Return the max power sensor entity id""" + return self._max_power_sensor_entity_id + + @property + def started_vtherm_total_power(self) -> float | None: + """Return the started_vtherm_total_power""" + return sum(self._started_vtherm_total_power_by_id.values()) + + @property + def is_detected(self) -> bool: + """True if the central power management is detected""" + return self._state + + def __str__(self): + return "CentralPowerManager" diff --git a/custom_components/versatile_thermostat/feature_heating_failure_detection_manager.py b/custom_components/versatile_thermostat/feature_heating_failure_detection_manager.py new file mode 100644 index 0000000..340f05f --- /dev/null +++ b/custom_components/versatile_thermostat/feature_heating_failure_detection_manager.py @@ -0,0 +1,615 @@ +# pylint: disable=line-too-long + +""" Implements the Heating Failure Detection as a Feature Manager""" + +import logging +from vtherm_api.log_collector import get_vtherm_logger +from typing import Any +from datetime import datetime, timedelta + +from homeassistant.const import ( + STATE_ON, + STATE_OFF, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.template import Template + +from .const import ( + CONF_USE_HEATING_FAILURE_DETECTION_FEATURE, + CONF_HEATING_FAILURE_THRESHOLD, + CONF_COOLING_FAILURE_THRESHOLD, + CONF_HEATING_FAILURE_DETECTION_DELAY, + CONF_TEMPERATURE_CHANGE_TOLERANCE, + CONF_FAILURE_DETECTION_ENABLE_TEMPLATE, + DEFAULT_HEATING_FAILURE_THRESHOLD, + DEFAULT_COOLING_FAILURE_THRESHOLD, + DEFAULT_HEATING_FAILURE_DETECTION_DELAY, + DEFAULT_TEMPERATURE_CHANGE_TOLERANCE, + EventType, + overrides, +) +from .commons import write_event_log +from .commons_type import ConfigData + +from .base_manager import BaseFeatureManager +from .vtherm_hvac_mode import VThermHvacMode_OFF, VThermHvacMode_HEAT + +_LOGGER = get_vtherm_logger(__name__) + + +class FeatureHeatingFailureDetectionManager(BaseFeatureManager): + """The implementation of the Heating Failure Detection feature + + This feature detects: + 1. Heating failure: high on_percent but temperature not increasing + 2. Cooling failure: on_percent at 0 but temperature still increasing + + When a failure is detected on a thermostat with valve underlyings, + root cause analysis is performed by comparing each underlying's + should_device_be_active (commanded state) vs is_device_active (real state) + to determine if the failure is caused by a stuck valve. + """ + + unrecorded_attributes = frozenset( + { + "heating_failure_threshold", + "cooling_failure_threshold", + "heating_failure_detection_delay", + "temperature_change_tolerance", + "is_heating_failure_detection_configured", + "failure_detection_enable_template", + } + ) + + def __init__(self, vtherm: Any, hass: HomeAssistant): + """Init of a featureManager""" + super().__init__(vtherm, hass) + + self._is_configured: bool = False + self._heating_failure_threshold: float = DEFAULT_HEATING_FAILURE_THRESHOLD + self._cooling_failure_threshold: float = DEFAULT_COOLING_FAILURE_THRESHOLD + self._heating_failure_detection_delay: int = DEFAULT_HEATING_FAILURE_DETECTION_DELAY + self._temperature_change_tolerance: float = DEFAULT_TEMPERATURE_CHANGE_TOLERANCE + self._failure_detection_enable_template: Template | None = None + + # State tracking + self._heating_failure_state: str = STATE_UNAVAILABLE + self._cooling_failure_state: str = STATE_UNAVAILABLE + + # Temperature tracking for failure detection + self._last_check_time: datetime | None = None + self._last_check_temperature: float | None = None + self._high_power_start_time: datetime | None = None + self._zero_power_start_time: datetime | None = None + + @overrides + def post_init(self, entry_infos: ConfigData): + """Reinit of the manager""" + + use_feature = entry_infos.get(CONF_USE_HEATING_FAILURE_DETECTION_FEATURE, False) + + if not use_feature: + self._is_configured = False + self._heating_failure_state = STATE_UNAVAILABLE + self._cooling_failure_state = STATE_UNAVAILABLE + return + + self._heating_failure_threshold = entry_infos.get( + CONF_HEATING_FAILURE_THRESHOLD, + DEFAULT_HEATING_FAILURE_THRESHOLD + ) + self._cooling_failure_threshold = entry_infos.get( + CONF_COOLING_FAILURE_THRESHOLD, + DEFAULT_COOLING_FAILURE_THRESHOLD + ) + self._heating_failure_detection_delay = entry_infos.get( + CONF_HEATING_FAILURE_DETECTION_DELAY, + DEFAULT_HEATING_FAILURE_DETECTION_DELAY + ) + self._temperature_change_tolerance = entry_infos.get(CONF_TEMPERATURE_CHANGE_TOLERANCE, DEFAULT_TEMPERATURE_CHANGE_TOLERANCE) + + # Initialize the enable template if provided + template_str = entry_infos.get(CONF_FAILURE_DETECTION_ENABLE_TEMPLATE) + if template_str: + self._failure_detection_enable_template = Template(template_str, self._hass) + else: + self._failure_detection_enable_template = None + + self._is_configured = True + self._heating_failure_state = STATE_UNKNOWN + self._cooling_failure_state = STATE_UNKNOWN + + @overrides + async def start_listening(self): + """Start listening the underlying entity""" + # No external entity to listen to for this feature + + @overrides + def stop_listening(self): + """Stop listening and remove the eventual timer still running""" + + def _is_detection_enabled_by_template(self) -> bool: + """Evaluate the enable template to determine if detection should be active. + Returns True if detection is enabled (template returns True or no template configured). + Returns False if template returns False. + If template evaluation fails, returns True (detection enabled) as a safety measure. + """ + if self._failure_detection_enable_template is None: + # No template configured, detection is always enabled + return True + + try: + result = self._failure_detection_enable_template.async_render() + # Handle various truthy/falsy values + if isinstance(result, bool): + return result + if isinstance(result, str): + return result.lower() in ("true", "1", "yes", "on") + return bool(result) + except Exception as err: # pylint: disable=broad-except + _LOGGER.warning( + "%s - Error evaluating failure_detection_enable_template: %s. Detection remains enabled.", + self, err + ) + # On error, enable detection as a safety measure + return True + + @overrides + async def refresh_state(self) -> bool: + """Check for heating/cooling failures + Return True if a failure is detected""" + + if not self._is_configured: + _LOGGER.debug("%s - heating failure detection is disabled (or not configured)", self) + return False + + # Only check for VTherms with proportional algorithm + if not self._vtherm.has_prop: + _LOGGER.debug("%s - heating failure detection skipped (no proportional algorithm)", self) + return False + + if self._vtherm.requested_state.hvac_mode == VThermHvacMode_OFF: + self._reset_tracking() + self._heating_failure_state = STATE_OFF + self._cooling_failure_state = STATE_OFF + _LOGGER.debug("%s - heating failure detection is OFF because requested_state is OFF", self) + return False + + # Check if detection is enabled by template + if not self._is_detection_enabled_by_template(): + _LOGGER.debug("%s - heating failure detection is disabled by template", self) + # Reset tracking when template disables detection + self._reset_tracking() + # Keep states at OFF when disabled by template (not UNAVAILABLE since feature is configured) + if self._heating_failure_state == STATE_ON: + self._heating_failure_state = STATE_OFF + self._send_heating_failure_event("heating_failure_end", 0, 0, self._vtherm.current_temperature or 0) + if self._cooling_failure_state == STATE_ON: + self._cooling_failure_state = STATE_OFF + self._send_cooling_failure_event("cooling_failure_end", 0, 0, self._vtherm.current_temperature or 0) + return False + + now = self._vtherm.now + + # Get current values + current_temp = self._vtherm.current_temperature + on_percent = self._vtherm.on_percent + + if current_temp is None or on_percent is None: + _LOGGER.debug("%s - heating failure detection skipped (no temp or on_percent)", self) + return False + + # Determine the mode (heating or cooling) + is_heating_mode = self._vtherm.vtherm_hvac_mode == VThermHvacMode_HEAT + + # Initialize tracking if needed + if self._last_check_time is None: + self._last_check_time = now + self._last_check_temperature = current_temp + return False + + detection_delay_td = timedelta(minutes=self._heating_failure_detection_delay) + + # Check for heating and cooling failures + if is_heating_mode: + self._check_heating_failure(now, current_temp, on_percent, detection_delay_td) + self._check_cooling_failure(now, current_temp, on_percent, detection_delay_td) + + # Initialize states if still unknown + if self._heating_failure_state == STATE_UNKNOWN: + self._heating_failure_state = STATE_OFF + if self._cooling_failure_state == STATE_UNKNOWN: + self._cooling_failure_state = STATE_OFF + + return self.is_failure_detected + + def _check_heating_failure(self, now: datetime, current_temp: float, on_percent: float, detection_delay_td: timedelta): + """Check for HEATING failure: + High on_percent (>= threshold) but temperature not increasing""" + + old_heating_failure = self._heating_failure_state == STATE_ON + + if on_percent >= self._heating_failure_threshold: + if self._high_power_start_time is None: + self._high_power_start_time = now + self._last_check_temperature = current_temp + _LOGGER.debug( + "%s - Starting high power tracking (on_percent=%.2f >= threshold=%.2f)", + self, on_percent, self._heating_failure_threshold + ) + else: + elapsed = now - self._high_power_start_time + if elapsed >= detection_delay_td: + # Check if temperature has increased enough (above tolerance) + if self._last_check_temperature is not None: + temp_diff = current_temp - self._last_check_temperature + if temp_diff < self._temperature_change_tolerance: + # Temperature not increasing enough - heating failure detected + if not old_heating_failure: + _LOGGER.warning( + "%s - Heating failure detected: on_percent=%.2f%%, temp_diff=%.2f° (tolerance=%.2f°) over %d minutes", + self, + on_percent * 100, + temp_diff, + self._temperature_change_tolerance, + self._heating_failure_detection_delay, + ) + self._heating_failure_state = STATE_ON + self._send_heating_failure_event("heating_failure_start", on_percent, temp_diff, current_temp) + else: + # Temperature is increasing enough, reset if we were in failure + if old_heating_failure: + _LOGGER.info("%s - Heating failure ended: temperature is now increasing", self) + self._heating_failure_state = STATE_OFF + self._send_heating_failure_event("heating_failure_end", on_percent, temp_diff, current_temp) + self._high_power_start_time = now + self._last_check_temperature = current_temp + else: + # Not in high power, reset tracking + if self._high_power_start_time is not None: + self._high_power_start_time = None + if old_heating_failure: + self._heating_failure_state = STATE_OFF + self._send_heating_failure_event("heating_failure_end", on_percent, 0, current_temp) + + def _check_cooling_failure(self, now: datetime, current_temp: float, on_percent: float, detection_delay_td: timedelta): + """Check for COOLING failure: + on_percent at 0 (or <= cooling threshold) but temperature still increasing""" + + old_cooling_failure = self._cooling_failure_state == STATE_ON + + if on_percent <= self._cooling_failure_threshold: + if self._zero_power_start_time is None: + self._zero_power_start_time = now + self._last_check_temperature = current_temp + _LOGGER.debug( + "%s - Starting zero power tracking (on_percent=%.2f <= threshold=%.2f)", + self, on_percent, self._cooling_failure_threshold + ) + else: + elapsed = now - self._zero_power_start_time + if elapsed >= detection_delay_td: + # Check if temperature is increasing above tolerance + if self._last_check_temperature is not None: + temp_diff = current_temp - self._last_check_temperature + if temp_diff > self._temperature_change_tolerance: + # Temperature still increasing despite no heating - cooling failure + if not old_cooling_failure: + _LOGGER.warning( + "%s - Cooling failure detected: on_percent=%.2f%%, temp_diff=+%.2f° (tolerance=%.2f°) over %d minutes", + self, + on_percent * 100, + temp_diff, + self._temperature_change_tolerance, + self._heating_failure_detection_delay, + ) + self._cooling_failure_state = STATE_ON + self._send_cooling_failure_event("cooling_failure_start", on_percent, temp_diff, current_temp) + else: + # Temperature is stable or decreasing, reset if we were in failure + if old_cooling_failure: + _LOGGER.info("%s - Cooling failure ended: temperature is now stable or decreasing", self) + self._cooling_failure_state = STATE_OFF + self._send_cooling_failure_event("cooling_failure_end", on_percent, temp_diff, current_temp) + self._zero_power_start_time = now + self._last_check_temperature = current_temp + else: + # Not at zero power, reset tracking + if self._zero_power_start_time is not None: + self._zero_power_start_time = None + if old_cooling_failure: + self._cooling_failure_state = STATE_OFF + self._send_cooling_failure_event("cooling_failure_end", on_percent, 0, current_temp) + + def _reset_tracking(self): + """Reset all tracking variables""" + self._last_check_time = None + self._last_check_temperature = None + self._high_power_start_time = None + self._zero_power_start_time = None + + def _diagnose_root_cause(self, failure_type: str) -> dict[str, Any]: + """Diagnose the root cause of a failure by checking valve underlyings. + + For thermostats with valve underlyings (over_valve or over_climate_valve), + compares the requested state (should_device_be_active) with the real state + (is_device_active) to determine if a stuck valve is the cause. + + Args: + failure_type: "heating" or "cooling" + + Returns: + A dict with root_cause, root_cause_entity_id, and root_cause_details + """ + result = { + "root_cause": "not_identified", + "root_cause_entity_id": None, + "root_cause_details": [], + } + + if not hasattr(self._vtherm, '_underlyings'): + return result + + from .underlyings import UnderlyingValve + + stuck_valves = [] + for under in self._vtherm._underlyings: + if not isinstance(under, UnderlyingValve): + continue + + should_active = under.should_device_be_active + is_active = under.is_device_active + + if should_active is None or is_active is None: + continue + + if should_active and not is_active: + stuck_valves.append({ + "entity_id": under.entity_id, + "type": "valve_stuck_closed", + "requested": "open", + "actual": "closed", + }) + elif not should_active and is_active: + stuck_valves.append({ + "entity_id": under.entity_id, + "type": "valve_stuck_open", + "requested": "closed", + "actual": "open", + }) + + if stuck_valves: + if failure_type == "heating": + valve_stuck_type = "valve_stuck_closed" + else: + valve_stuck_type = "valve_stuck_open" + + matching = [v for v in stuck_valves if v["type"] == valve_stuck_type] + if matching: + result["root_cause"] = valve_stuck_type + result["root_cause_entity_id"] = matching[0]["entity_id"] + result["root_cause_details"] = matching + else: + result["root_cause"] = stuck_valves[0]["type"] + result["root_cause_entity_id"] = stuck_valves[0]["entity_id"] + result["root_cause_details"] = stuck_valves + + return result + + def _send_heating_failure_event(self, event_type: str, on_percent: float, temp_diff: float, current_temp: float): + """Send a heating failure event""" + is_enabled_by_template = self._is_detection_enabled_by_template() + + root_cause_info = self._diagnose_root_cause("heating") if event_type == "heating_failure_start" else { + "root_cause": "not_identified", + "root_cause_entity_id": None, + "root_cause_details": [], + } + + # Log the event + if event_type == "heating_failure_start": + write_event_log( + _LOGGER, self._vtherm, + f"Heating failure detected: on_percent={on_percent*100:.0f}%, temp_diff={temp_diff:.2f}°, root_cause={root_cause_info['root_cause']}" + ) + else: + write_event_log( + _LOGGER, self._vtherm, f"Heating failure ended: on_percent={on_percent*100:.0f}%, temp_diff={temp_diff:.2f}°, template_enabled={is_enabled_by_template}" + ) + + self._vtherm.send_event( + EventType.HEATING_FAILURE_EVENT, + { + "type": event_type, + "failure_type": "heating", + "on_percent": on_percent, + "temperature_difference": temp_diff, + "current_temp": current_temp, + "target_temp": self._vtherm.target_temperature, + "threshold": self._heating_failure_threshold, + "detection_delay_min": self._heating_failure_detection_delay, + "is_enabled_by_template": is_enabled_by_template, + "root_cause": root_cause_info["root_cause"], + "root_cause_entity_id": root_cause_info["root_cause_entity_id"], + "root_cause_details": root_cause_info["root_cause_details"], + }, + ) + + def _send_cooling_failure_event(self, event_type: str, on_percent: float, temp_diff: float, current_temp: float): + """Send a cooling failure event""" + is_enabled_by_template = self._is_detection_enabled_by_template() + + root_cause_info = self._diagnose_root_cause("cooling") if event_type == "cooling_failure_start" else { + "root_cause": "not_identified", + "root_cause_entity_id": None, + "root_cause_details": [], + } + + # Log the event + if event_type == "cooling_failure_start": + write_event_log( + _LOGGER, self._vtherm, + f"Cooling failure detected: on_percent={on_percent*100:.0f}%, temp_diff=+{temp_diff:.2f}°, root_cause={root_cause_info['root_cause']}" + ) + else: + write_event_log( + _LOGGER, self._vtherm, f"Cooling failure ended: on_percent={on_percent*100:.0f}%, temp_diff={temp_diff:.2f}°, template_enabled={is_enabled_by_template}" + ) + + self._vtherm.send_event( + EventType.HEATING_FAILURE_EVENT, + { + "type": event_type, + "failure_type": "cooling", + "on_percent": on_percent, + "temperature_difference": temp_diff, + "current_temp": current_temp, + "target_temp": self._vtherm.target_temperature, + "threshold": self._cooling_failure_threshold, + "detection_delay_min": self._heating_failure_detection_delay, + "is_enabled_by_template": is_enabled_by_template, + "root_cause": root_cause_info["root_cause"], + "root_cause_entity_id": root_cause_info["root_cause_entity_id"], + "root_cause_details": root_cause_info["root_cause_details"], + }, + ) + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): + """Add some custom attributes""" + + extra_state_attributes.update( + { + "is_heating_failure_detection_configured": self._is_configured, + } + ) + + if self._is_configured: + # Calculate remaining time for heating failure detection + heating_tracking_info = self._get_tracking_info(self._high_power_start_time, "heating") + # Calculate remaining time for cooling failure detection + cooling_tracking_info = self._get_tracking_info(self._zero_power_start_time, "cooling") + + # Get template status + template_str = None + template_enabled = True + if self._failure_detection_enable_template is not None: + template_str = self._failure_detection_enable_template.template + template_enabled = self._is_detection_enabled_by_template() + + root_cause_info = {} + if self._heating_failure_state == STATE_ON: + root_cause_info = self._diagnose_root_cause("heating") + elif self._cooling_failure_state == STATE_ON: + root_cause_info = self._diagnose_root_cause("cooling") + + extra_state_attributes.update( + { + "heating_failure_detection_manager": { + "heating_failure_state": self._heating_failure_state, + "cooling_failure_state": self._cooling_failure_state, + "heating_failure_threshold": self._heating_failure_threshold, + "cooling_failure_threshold": self._cooling_failure_threshold, + "detection_delay_min": self._heating_failure_detection_delay, + "temperature_change_tolerance": self._temperature_change_tolerance, + "failure_detection_enable_template": template_str, + "is_detection_enabled_by_template": template_enabled, + "heating_tracking": heating_tracking_info, + "cooling_tracking": cooling_tracking_info, + "root_cause": root_cause_info.get("root_cause"), + "root_cause_entity_id": root_cause_info.get("root_cause_entity_id"), + } + } + ) + + def _get_tracking_info(self, start_time: datetime | None, tracking_type: str) -> dict[str, Any]: + """Get tracking information for heating or cooling failure detection + + Args: + start_time: The start time of the tracking (high_power or zero_power) + tracking_type: "heating" or "cooling" for logging purposes + + Returns: + A dictionary with tracking status information + """ + if start_time is None: + return { + "is_tracking": False, + "initial_temperature": None, + "current_temperature": None, + "remaining_time_min": None, + "elapsed_time_min": None, + } + + now = self._vtherm.now + elapsed = now - start_time + elapsed_minutes = elapsed.total_seconds() / 60 + remaining_minutes = max(0, self._heating_failure_detection_delay - elapsed_minutes) + + return { + "is_tracking": True, + "initial_temperature": self._last_check_temperature, + "current_temperature": self._vtherm.current_temperature, + "remaining_time_min": round(remaining_minutes, 1), + "elapsed_time_min": round(elapsed_minutes, 1), + } + + @overrides + @property + def is_configured(self) -> bool: + """Return True if the heating failure detection feature is configured""" + return self._is_configured + + @property + def is_failure_detected(self) -> bool: + """Returns True if any failure is currently detected""" + return self._heating_failure_state == STATE_ON or self._cooling_failure_state == STATE_ON + + @property + def is_detected(self) -> bool: + """Return the overall state of the feature manager based on failure states""" + return self.is_failure_detected + + @property + def is_heating_failure_detected(self) -> bool: + """Returns True if a heating failure is detected""" + return self._heating_failure_state == STATE_ON + + @property + def is_cooling_failure_detected(self) -> bool: + """Returns True if a cooling failure is detected""" + return self._cooling_failure_state == STATE_ON + + @property + def heating_failure_state(self) -> str: + """Returns the heating failure state: STATE_ON, STATE_OFF, STATE_UNKNOWN, STATE_UNAVAILABLE""" + return self._heating_failure_state + + @property + def cooling_failure_state(self) -> str: + """Returns the cooling failure state: STATE_ON, STATE_OFF, STATE_UNKNOWN, STATE_UNAVAILABLE""" + return self._cooling_failure_state + + @property + def heating_failure_threshold(self) -> float: + """Returns the heating failure threshold""" + return self._heating_failure_threshold + + @property + def cooling_failure_threshold(self) -> float: + """Returns the cooling failure threshold""" + return self._cooling_failure_threshold + + @property + def detection_delay_min(self) -> int: + """Returns the detection delay in minutes""" + return self._heating_failure_detection_delay + + @property + def temperature_change_tolerance(self) -> float: + """Returns the temperature change tolerance in degrees""" + return self._temperature_change_tolerance + + def __str__(self): + return f"HeatingFailureDetectionManager-{self.name}" diff --git a/custom_components/versatile_thermostat/feature_lock_manager.py b/custom_components/versatile_thermostat/feature_lock_manager.py new file mode 100644 index 0000000..71dfbc2 --- /dev/null +++ b/custom_components/versatile_thermostat/feature_lock_manager.py @@ -0,0 +1,152 @@ +""" This module manages the lock feature of the Versatile Thermostat integration. """ +import logging +from datetime import timedelta +from vtherm_api.log_collector import get_vtherm_logger +from typing import Any + +from homeassistant.core import ( + HomeAssistant, + callback, +) + +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.event import async_call_later + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons_type import ConfigData + +from .base_manager import BaseFeatureManager + +_LOGGER = get_vtherm_logger(__name__) + +class FeatureLockManager(BaseFeatureManager): + """ The implementation of the Lock Feature Manager for Versatile Thermostat """ + def __init__(self, vtherm: Any, hass: HomeAssistant): + """Initialize the FeatureLockManager.""" + super().__init__(vtherm, hass) + self._is_configured: bool = False + self._lock_users: bool = True + self._lock_automations: bool = True + self._lock_code: str | None = None + self._is_locked: bool = False + self._auto_relock_sec: int = 30 + self._cancel_auto_relock = None + + @overrides + def post_init(self, entry_infos: ConfigData): + """Reinit of the manager""" + self._lock_users = entry_infos.get(CONF_LOCK_USERS, True) + self._lock_automations = entry_infos.get(CONF_LOCK_AUTOMATIONS, True) + self._lock_code = entry_infos.get(CONF_LOCK_CODE) + self._auto_relock_sec = int(entry_infos.get(CONF_AUTO_RELOCK_SEC, 30) or 0) + self._is_configured = self._lock_users or self._lock_automations + + @overrides + def restore_state(self, old_state) -> None: + """Restore locks from old state.""" + if old_state is not None: + self._is_locked = bool(old_state.attributes.get("specific_states", {}).get("is_locked", False)) + + @overrides + async def start_listening(self): + """Nothing to listen here""" + + @property + def is_configured(self) -> bool: + """True if the FeatureManager is fully configured""" + return self._is_configured + + @property + def is_locked(self) -> bool: + """Return True if the thermostat is locked.""" + return self._is_locked + + @property + def is_detected(self) -> bool: + """Return the overall state of the feature manager based on failure states""" + return self.is_locked + + def check_is_locked(self, function_name: str) -> bool: + """Check if the thermostat is locked.""" + context = getattr(self._vtherm, "_context", None) + source_is_user = context and context.user_id is not None + source_is_automation = not source_is_user + + if self._is_locked and ( + (self._lock_users and source_is_user) + or (self._lock_automations and source_is_automation) + ): + _LOGGER.info( + "%s - Blocked external call to %s while locked (source=%s)", + self, + function_name, + "user" if source_is_user else "automation/unknown", + ) + return True + return False + + def _validate_lock_code(self, code: str | None) -> bool: + """Validate the provided code against the configured lock code.""" + if self._lock_code: + if not code or str(code) != str(self._lock_code): + _LOGGER.error("%s - Lock code validation failed", self) + raise HomeAssistantError(f"Lock code validation failed: {code}") + return True + + def change_lock_state(self, locked: bool, code: str | None = None) -> None: + """Set the internal lock state.""" + if self._validate_lock_code(code): + if self._cancel_auto_relock: + self._cancel_auto_relock() + self._cancel_auto_relock = None + self._is_locked = locked + _LOGGER.info("%s - Lock state set to %s", self, locked) + if not locked and self._auto_relock_sec > 0: + self._cancel_auto_relock = async_call_later( + self.hass, + timedelta(seconds=self._auto_relock_sec), + self._do_auto_relock, + ) + _LOGGER.info( + "%s - Auto-relock scheduled in %s seconds", + self, + self._auto_relock_sec, + ) + return True + return False + + @callback + def _do_auto_relock(self, _now) -> None: + """Callback triggered by the auto-relock timer.""" + self._cancel_auto_relock = None + self._is_locked = True + _LOGGER.info( + "%s - Auto-relock triggered after %s seconds", self, self._auto_relock_sec + ) + self._vtherm.update_custom_attributes() + self._vtherm.async_write_ha_state() + + @property + def has_lock_settings_enabled(self) -> bool: + """Return True if any lock setting is enabled.""" + return self._is_configured and (self._lock_users or self._lock_automations) + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]) -> None: + """Add custom attributes to the attributes dict.""" + + extra_state_attributes.update( + { + "is_lock_configured": self._is_configured, + } + ) + if self._is_configured: + extra_state_attributes.update( + { + "lock_manager": { + "is_locked": self._is_locked, + "lock_users": self._lock_users, + "lock_automations": self._lock_automations, + "lock_code": bool(self._lock_code), + "auto_relock_sec": self._auto_relock_sec, + } + }) diff --git a/custom_components/versatile_thermostat/feature_motion_manager.py b/custom_components/versatile_thermostat/feature_motion_manager.py new file mode 100644 index 0000000..85106e5 --- /dev/null +++ b/custom_components/versatile_thermostat/feature_motion_manager.py @@ -0,0 +1,340 @@ +""" Implements the Motion Feature Manager """ + +# pylint: disable=line-too-long + +from typing import Any +from datetime import timedelta + +from homeassistant.const import ( + STATE_ON, + STATE_OFF, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import ( + HomeAssistant, + callback, + Event, +) +from homeassistant.helpers.event import ( + async_track_state_change_event, + EventStateChangedData, + async_call_later, +) + +from homeassistant.exceptions import ConditionError +from homeassistant.helpers import condition + +from vtherm_api.log_collector import get_vtherm_logger +from .vtherm_preset import VThermPreset + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons import write_event_log +from .commons_type import ConfigData + +from .base_manager import BaseFeatureManager + +_LOGGER = get_vtherm_logger(__name__) + + +class FeatureMotionManager(BaseFeatureManager): + """The implementation of the Motion feature""" + + unrecorded_attributes = frozenset( + { + "motion_sensor_entity_id", + "is_motion_configured", + "motion_delay_sec", + "motion_off_delay_sec", + "motion_preset", + "no_motion_preset", + } + ) + + def __init__(self, vtherm: Any, hass: HomeAssistant): + """Init of a featureManager""" + super().__init__(vtherm, hass) + self._motion_state: str = STATE_UNAVAILABLE + self._motion_sensor_entity_id: str = None + self._motion_delay_sec: int | None = 0 + self._motion_off_delay_sec: int | None = 0 + self._motion_preset: str | None = None + self._no_motion_preset: str | None = None + self._is_configured: bool = False + self._motion_call_cancel: callable = None + + @overrides + def post_init(self, entry_infos: ConfigData): + """Reinit of the manager""" + self.dearm_motion_timer() + + self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR, None) + self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY, 0) + self._motion_off_delay_sec = entry_infos.get(CONF_MOTION_OFF_DELAY, None) + if not self._motion_off_delay_sec: + self._motion_off_delay_sec = self._motion_delay_sec + + self._motion_preset = entry_infos.get(CONF_MOTION_PRESET) + self._no_motion_preset = entry_infos.get(CONF_NO_MOTION_PRESET) + if ( + self._motion_sensor_entity_id is not None + and self._motion_preset is not None + and self._no_motion_preset is not None + ): + self._is_configured = True + self._motion_state = STATE_UNKNOWN + + @overrides + async def start_listening(self): + """Start listening the underlying entity""" + if self._is_configured: + self.stop_listening() + self.add_listener( + async_track_state_change_event( + self.hass, + [self._motion_sensor_entity_id], + self._motion_sensor_changed, + ) + ) + + @overrides + def stop_listening(self): + """Stop listening and remove the eventual timer still running""" + self.dearm_motion_timer() + super().stop_listening() + + def dearm_motion_timer(self): + """Dearm the eventual motion time running""" + if self._motion_call_cancel: + self._motion_call_cancel() + self._motion_call_cancel = None + + @overrides + async def refresh_state(self) -> bool: + """Tries to get the last state from sensor + Returns True if a change has been made""" + ret = False + if self._is_configured: + + motion_state = self.hass.states.get(self._motion_sensor_entity_id) + if motion_state and motion_state.state not in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + _LOGGER.debug( + "%s - Motion state have been retrieved: %s", + self, + self._motion_state, + ) + # recalculate the right target_temp in activity mode + ret = await self.update_motion_state(motion_state.state) # , False) + + return ret + + @callback + async def _motion_sensor_changed(self, event: Event[EventStateChangedData]): + """Handle motion sensor changes.""" + new_state = event.data.get("new_state") + _LOGGER.info( + "%s - Motion changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s", + self, + new_state, + self._vtherm.preset_mode, + VThermPreset.ACTIVITY, + ) + + if new_state is None or new_state.state not in (STATE_OFF, STATE_ON): + return + + # Check delay condition + async def try_motion_condition(_): + self.dearm_motion_timer() + + try: + delay = ( + self._motion_delay_sec + if new_state.state == STATE_ON + else self._motion_off_delay_sec + ) + long_enough = condition.state( + self.hass, + self._motion_sensor_entity_id, + new_state.state, + timedelta(seconds=delay), + ) + except ConditionError: + long_enough = False + + if not long_enough: + _LOGGER.debug( + "Motion delay condition is not satisfied (the sensor have change its state during the delay). Check motion sensor state" + ) + # 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) + await self.update_motion_state(new_state.state) + else: + await self.update_motion_state( + STATE_ON if new_state.state == STATE_OFF else STATE_OFF + ) + + im_on = self._motion_state == STATE_ON + delay_running = self._motion_call_cancel is not None + event_on = new_state.state == STATE_ON + + def arm(): + """Arm the timer""" + delay = ( + self._motion_delay_sec + if new_state.state == STATE_ON + else self._motion_off_delay_sec + ) + self._motion_call_cancel = async_call_later( + self.hass, timedelta(seconds=delay), try_motion_condition + ) + + # if I'm off + if not im_on: + if event_on and not delay_running: + _LOGGER.debug( + "%s - Arm delay cause i'm off and event is on and no delay is running", + self, + ) + arm() + return try_motion_condition + # Ignore the event + _LOGGER.debug("%s - Event ignored cause i'm already off", self) + return None + else: # I'm On + if not event_on and not delay_running: + _LOGGER.info("%s - Arm delay cause i'm on and event is off", self) + arm() + return try_motion_condition + if event_on and delay_running: + _LOGGER.debug( + "%s - Desarm off delay cause i'm on and event is on and a delay is running", + self, + ) + self.dearm_motion_timer() + return None + # Ignore the event + _LOGGER.debug("%s - Event ignored cause i'm already on", self) + return None + + async def update_motion_state(self, new_state: str = None) -> bool: # , recalculate: bool = True) -> bool: + """Update the value of the motion sensor and update the VTherm state accordingly + Return true if a change has been made""" + + _LOGGER.info("%s - Updating motion state. New state is %s", self, new_state) + old_motion_state = self._motion_state + if new_state is not None: + self._motion_state = STATE_ON if new_state == STATE_ON else STATE_OFF + + if old_motion_state != self._motion_state: + write_event_log(_LOGGER, self._vtherm, f"Motion state changed from {old_motion_state} to {self._motion_state}") + self._vtherm.requested_state.force_changed() + await self._vtherm.update_states(True) + return True + + return False + + def get_current_motion_preset(self) -> str: + """Calculate and return the current motion preset""" + return ( + self._motion_preset + if self._motion_state == STATE_ON + else self._no_motion_preset + ) + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): + """Add some custom attributes""" + extra_state_attributes.update( + { + "is_motion_configured": self._is_configured, + } + ) + if self._is_configured: + extra_state_attributes.update( + { + "motion_manager": { + "motion_sensor_entity_id": self._motion_sensor_entity_id, + "motion_state": self._motion_state, + "motion_delay_sec": self._motion_delay_sec, + "motion_off_delay_sec": self._motion_off_delay_sec, + "motion_preset": self._motion_preset, + "no_motion_preset": self._no_motion_preset, + } + } + ) + + @overrides + @property + def is_configured(self) -> bool: + """Return True of the motion is configured""" + return self._is_configured + + @property + def motion_state(self) -> str | None: + """Return the current motion state STATE_ON or STATE_OFF + or STATE_UNAVAILABLE if not configured""" + if not self._is_configured: + return STATE_UNAVAILABLE + return self._motion_state + + @property + def is_motion_detected(self) -> bool: + """Return true if the motion is configured and motion sensor is OFF""" + return self._is_configured and self._motion_state in [ + STATE_ON, + ] + + @property + def is_detected(self) -> bool: + """Return the overall state of the feature manager based on motion states""" + return self.is_motion_detected + + @property + def motion_sensor_entity_id(self) -> str | None: + """Return the entity ID of the motion sensor.""" + return self._motion_sensor_entity_id + + @property + def motion_delay_sec(self) -> int: + """Return the motion delay""" + return self._motion_delay_sec + + @property + def motion_off_delay_sec(self) -> int: + """Return motion delay off""" + return self._motion_off_delay_sec + + @property + def motion_preset(self) -> str | None: + """Return motion preset""" + return self._motion_preset + + @property + def no_motion_preset(self) -> str | None: + """Return no motion preset""" + return self._no_motion_preset + + def __str__(self): + return f"MotionManager-{self.name}" diff --git a/custom_components/versatile_thermostat/feature_power_manager.py b/custom_components/versatile_thermostat/feature_power_manager.py new file mode 100644 index 0000000..770134d --- /dev/null +++ b/custom_components/versatile_thermostat/feature_power_manager.py @@ -0,0 +1,368 @@ +""" Implements the Power Feature Manager """ + +# pylint: disable=line-too-long + +from typing import Any + +from homeassistant.const import ( + STATE_ON, + STATE_OFF, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) + +from homeassistant.core import ( + HomeAssistant, +) + +from vtherm_api.log_collector import get_vtherm_logger +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons import write_event_log, round_to_nearest +from .commons_type import ConfigData + +from .base_manager import BaseFeatureManager +from .vtherm_central_api import VersatileThermostatAPI + +_LOGGER = get_vtherm_logger(__name__) + + +class FeaturePowerManager(BaseFeatureManager): + """The implementation of the Power feature""" + + unrecorded_attributes = frozenset( + { + "power_sensor_entity_id", + "max_power_sensor_entity_id", + "is_power_configured", + "device_power", + "power_temp", + "current_power", + "current_max_power", + } + ) + + def __init__(self, vtherm: Any, hass: HomeAssistant): + """Init of a featureManager""" + super().__init__(vtherm, hass) + self._power_temp: float | None = None + self._overpowering_state: str | None = None + self._is_configured: bool = False + self._device_power: float = 0 + self._use_power_feature: bool = False + + @overrides + def post_init(self, entry_infos: ConfigData): + """Reinit of the manager""" + + # Power management + self._power_temp = entry_infos.get(CONF_PRESET_POWER) + + self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0 + self._use_power_feature = entry_infos.get(CONF_USE_POWER_FEATURE, False) + self._is_configured = False + + @overrides + async def start_listening(self): + """Start listening the underlying entity. There is nothing to listen""" + central_power_configuration = ( + VersatileThermostatAPI.get_vtherm_api().central_power_manager.is_configured + ) + + if self._use_power_feature and self._device_power and central_power_configuration: + self._is_configured = True + # Try to restore _overpowering_state from previous state + old_state = await self._vtherm.async_get_last_state() + self._overpowering_state = ( + STATE_ON if old_state is not None and hasattr(old_state, "attributes") and old_state.attributes.get("overpowering_state") == STATE_ON else STATE_UNKNOWN + ) + else: + if self._use_power_feature: + if not central_power_configuration: + _LOGGER.warning( + "%s - Power management is not fully configured. You have to configure the central configuration power", + self, + ) + else: + _LOGGER.warning( + "%s - Power management is not fully configured. You have to configure the power feature of the VTherm", + self, + ) + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): + """Add some custom attributes""" + vtherm_api = VersatileThermostatAPI.get_vtherm_api() + extra_state_attributes.update( + { + "is_power_configured": self.is_configured, + } + ) + if self._is_configured: + extra_state_attributes.update( + { + "power_manager": { + "power_sensor_entity_id": vtherm_api.central_power_manager.power_sensor_entity_id, + "max_power_sensor_entity_id": vtherm_api.central_power_manager.max_power_sensor_entity_id, + "overpowering_state": self.overpowering_state, + "device_power": self._device_power, + "power_temp": self._power_temp, + "current_power": vtherm_api.central_power_manager.current_power, + "current_max_power": vtherm_api.central_power_manager.current_max_power, + "mean_cycle_power": self.mean_cycle_power, + } + } + ) + else: + extra_state_attributes.update( + { + "power_manager": { + "device_power": self._device_power, + "mean_cycle_power": self.mean_cycle_power, + } + } + ) + + async def check_power_available( + self, reservation_key: str | None = None + ) -> tuple[bool, float]: + """Check if the Vtherm can be started considering overpowering. + Returns True if no overpowering conditions are found. + If True the vtherm power is written into the temporay vtherm started + """ + + vtherm_api = VersatileThermostatAPI.get_vtherm_api() + if ( + not self._is_configured + or not vtherm_api.central_power_manager.is_configured + ): + return True, 0 + + effective_reservation_key = reservation_key or self._default_power_reservation_key + current_power = vtherm_api.central_power_manager.current_power + current_max_power = vtherm_api.central_power_manager.current_max_power + started_vtherm_total_power = vtherm_api.central_power_manager.started_vtherm_total_power + current_started_power = vtherm_api.central_power_manager.get_started_vtherm_power( + effective_reservation_key + ) + if ( + current_power is None + or current_max_power is None + or self._device_power is None + ): + _LOGGER.warning( + "%s - power not valued. check_power_available not available", self + ) + return True, 0 + + _LOGGER.debug( + "%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f", + self, + current_power, + current_max_power, + self._device_power, + ) + + startup_power = self.calculate_underlying_startup_power() + + ret = ( + current_power + + started_vtherm_total_power + - current_started_power + + startup_power + ) < current_max_power + if not ret: + _LOGGER.info( + "%s - there is not enough power available power=%.3f, max_power=%.3f started_power=%.3f current_started_power=%.3f startup_power=%.3f heater power=%.3f reservation_key=%s", + self, + current_power, + current_max_power, + started_vtherm_total_power, + current_started_power, + startup_power, + self._device_power, + effective_reservation_key, + ) + + return ret, startup_power + + def calculate_underlying_startup_power(self) -> float: + """Calculate the incremental startup power for a single underlying. + + This is the power to *reserve* between two power sensor measurements + when a new underlying is about to be turned on. It is deliberately + distinct from ``calculate_power_consumption_max()`` (used for global + shedding decisions) and intentionally ignores ``on_percent``: startup + is an instantaneous decision, not a cycle-average one. + """ + if not self._device_power: + return 0 + + # over_climate and mono-underlying over_switch: if the device is + # already active, its load is already reflected in current_power, so + # no additional reservation is needed (returning device_power here + # would double-count the load between two sensor refreshes). + if self._vtherm.is_over_climate: + return 0 if self._vtherm.is_device_active else self._device_power + + if self._vtherm.nb_underlying_entities <= 1: + return 0 if self._vtherm.is_device_active else self._device_power + + # Multi-underlying over_switch: each underlying contributes an + # independent incremental slice, regardless of whether the VTherm is + # globally "active". Starting a 2nd underlying while the 1st is on + # must still reserve its own device_power/n slice. + return self._device_power / self._vtherm.nb_underlying_entities + + def calculate_power_consumption_max(self) -> float: + """Calculate the maximum power consumption""" + power_consumption_max = 0 + if not self._vtherm.is_device_active: + if self._vtherm.is_over_climate: + power_consumption_max = self._device_power + else: + # if on_percent is not defined, we consider that the device can consume all its power in the worst case + on_percent = self._vtherm.safe_on_percent if self._vtherm.safe_on_percent is not None else 1 + + power_consumption_max = max( + self._device_power / self._vtherm.nb_underlying_entities, + self._device_power * on_percent, + ) + return power_consumption_max + + def add_power_consumption_to_central_power_manager( + self, + reservation_key: str | None = None, + ): + """ + Add the current power consumption to the central power manager. + """ + vtherm_api = VersatileThermostatAPI.get_vtherm_api() + if not self._is_configured or not vtherm_api.central_power_manager.is_configured: + return + + startup_power = self.calculate_underlying_startup_power() + + vtherm_api.central_power_manager.set_started_vtherm_power( + reservation_key or self._default_power_reservation_key, + startup_power, + ) + + def sub_power_consumption_to_central_power_manager( + self, + reservation_key: str | None = None, + ): + """ + Substract the current power consumption to the central power manager. + """ + vtherm_api = VersatileThermostatAPI.get_vtherm_api() + if not self._is_configured or not vtherm_api.central_power_manager.is_configured: + return + + vtherm_api.central_power_manager.set_started_vtherm_power( + reservation_key or self._default_power_reservation_key, + 0, + ) + + async def set_overpowering(self, overpowering: bool, power_consumption_max: float = 0): + """Force the overpowering state for the VTherm""" + + vtherm_api = VersatileThermostatAPI.get_vtherm_api() + current_power = vtherm_api.central_power_manager.current_power + current_max_power = vtherm_api.central_power_manager.current_max_power + + if overpowering and not self.is_overpowering_detected: + write_event_log(_LOGGER, self._vtherm, "Overpowering is detected") + _LOGGER.warning("%s - overpowering is detected.", self) + + self._overpowering_state = STATE_ON + + await self._vtherm.async_underlying_entity_turn_off() + self._vtherm.send_event( + EventType.POWER_EVENT, + { + "type": "start", + "current_power": current_power, + "device_power": self._device_power, + "current_max_power": current_max_power, + "current_power_consumption": power_consumption_max, + }, + ) + elif not overpowering and self.is_overpowering_detected: + write_event_log(_LOGGER, self._vtherm, "End of overpowering is detected") + _LOGGER.warning("%s - end of overpowering is detected.", self) + self._overpowering_state = STATE_OFF + + self._vtherm.send_event( + EventType.POWER_EVENT, + { + "type": "end", + "current_power": current_power, + "device_power": self._device_power, + "current_max_power": current_max_power, + }, + ) + elif not overpowering and self._overpowering_state != STATE_OFF: + # just set to not overpowering the state which was not set + self._overpowering_state = STATE_OFF + else: + # Nothing to do (already in the right state) + return + # self._vtherm.update_custom_attributes() + + @overrides + @property + def is_configured(self) -> bool: + """Return True of the presence is configured""" + return self._is_configured + + @property + def overpowering_state(self) -> str | None: + """Return the current overpowering state STATE_ON or STATE_OFF + or STATE_UNAVAILABLE if not configured""" + if not self._is_configured: + return STATE_UNAVAILABLE + return self._overpowering_state + + @property + def is_overpowering_detected(self) -> bool: + """Return True if the Vtherm is in overpowering state""" + return self._overpowering_state == STATE_ON + + @property + def is_detected(self) -> bool: + """Return the overall state of the feature manager based on detection states""" + return self.is_overpowering_detected + + @property + def power_temperature(self) -> float | None: + """Return the power temperature""" + return self._power_temp + + @property + def device_power(self) -> float: + """Return the device power""" + return self._device_power + + @property + def mean_cycle_power(self) -> float | None: + """Returns the mean power consumption during the cycle""" + if not self._device_power: + return None + + if self._vtherm.proportional_algorithm: + algo_on_percent = self._vtherm.proportional_algorithm.on_percent + if algo_on_percent is None: + return None + return float(round_to_nearest(self._device_power * algo_on_percent, 0.01)) + + if self._vtherm.is_over_climate: + return self._device_power if self._vtherm.is_device_active else 0.0 + + return None + + @property + def _default_power_reservation_key(self) -> str: + """Return a stable fallback key for temporary power reservations.""" + return getattr(self._vtherm, "entity_id", None) or self._vtherm.name + + def __str__(self): + return f"PowerManager-{self.name}" diff --git a/custom_components/versatile_thermostat/feature_presence_manager.py b/custom_components/versatile_thermostat/feature_presence_manager.py new file mode 100644 index 0000000..11ef17a --- /dev/null +++ b/custom_components/versatile_thermostat/feature_presence_manager.py @@ -0,0 +1,180 @@ +""" Implements the Presence Feature Manager """ + +# pylint: disable=line-too-long + +from typing import Any + +from homeassistant.const import ( + STATE_ON, + STATE_OFF, + STATE_HOME, + STATE_NOT_HOME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import ( + HomeAssistant, + callback, + Event, +) +from homeassistant.helpers.event import ( + async_track_state_change_event, + EventStateChangedData, +) + +from vtherm_api.log_collector import get_vtherm_logger +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons import write_event_log +from .commons_type import ConfigData +from .vtherm_preset import VThermPreset + +from .base_manager import BaseFeatureManager + +_LOGGER = get_vtherm_logger(__name__) + + +class FeaturePresenceManager(BaseFeatureManager): + """The implementation of the Presence feature""" + + unrecorded_attributes = frozenset( + { + "presence_sensor_entity_id", + "is_presence_configured", + } + ) + + def __init__(self, vtherm: Any, hass: HomeAssistant): + """Init of a featureManager""" + super().__init__(vtherm, hass) + self._presence_state: str = STATE_UNAVAILABLE + self._presence_sensor_entity_id: str = None + self._is_configured: bool = False + + @overrides + def post_init(self, entry_infos: ConfigData): + """Reinit of the manager""" + self._presence_sensor_entity_id = entry_infos.get(CONF_PRESENCE_SENSOR) + if ( + entry_infos.get(CONF_USE_PRESENCE_FEATURE, False) + and self._presence_sensor_entity_id is not None + ): + self._is_configured = True + self._presence_state = STATE_UNKNOWN + + @overrides + async def start_listening(self): + """Start listening the underlying entity""" + if self._is_configured: + self.stop_listening() + self.add_listener( + async_track_state_change_event( + self.hass, + [self._presence_sensor_entity_id], + self._presence_sensor_changed, + ) + ) + + @overrides + async def refresh_state(self) -> bool: + """Tries to get the last state from sensor + Returns True if a change has been made""" + ret = False + if self._is_configured: + # try to acquire presence entity state + presence_state = self.hass.states.get(self._presence_sensor_entity_id) + if presence_state and presence_state.state not in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + ret = await self.update_presence(presence_state.state) + _LOGGER.debug( + "%s - Presence have been retrieved: %s", + self, + presence_state.state, + ) + return ret + + @callback + async def _presence_sensor_changed(self, event: Event[EventStateChangedData]): + """Handle presence changes.""" + new_state = event.data.get("new_state") + write_event_log(_LOGGER, self._vtherm, f"Presence sensor changed to state {new_state.state if new_state else None}") + + if new_state is None: + return + + return await self.update_presence(new_state.state) + + async def update_presence(self, new_state: str): + """Update the value of the presence sensor and update the VTherm state accordingly""" + + _LOGGER.info("%s - Updating presence. New state is %s", self, new_state) + old_presence_state = self._presence_state + self._presence_state = STATE_ON if new_state in (STATE_ON, STATE_HOME) else STATE_OFF + + if new_state is None or new_state not in ( + STATE_OFF, + STATE_ON, + STATE_HOME, + STATE_NOT_HOME, + ): + self._presence_state = STATE_UNKNOWN + + if old_presence_state != self._presence_state: + self._vtherm.requested_state.force_changed() + await self._vtherm.update_states(True) + return True + + return False + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): + """Add some custom attributes""" + extra_state_attributes.update( + { + "is_presence_configured": self._is_configured, + } + ) + if self._is_configured: + extra_state_attributes.update( + { + "presence_manager": { + "presence_sensor_entity_id": self._presence_sensor_entity_id, + "presence_state": self._presence_state, + } + } + ) + + @overrides + @property + def is_configured(self) -> bool: + """Return True of the presence is configured""" + return self._is_configured + + @property + def presence_state(self) -> str | None: + """Return the current presence state STATE_ON or STATE_OFF + or STATE_UNAVAILABLE if not configured""" + if not self._is_configured: + return STATE_UNAVAILABLE + return self._presence_state + + @property + def is_absence_detected(self) -> bool: + """Return true if the presence is configured and presence sensor is OFF""" + return self._is_configured and self._presence_state in [ + STATE_NOT_HOME, + STATE_OFF, + ] + + @property + def is_detected(self) -> bool: + """Return the overall state of the feature manager based on presence states""" + return not self.is_absence_detected + + @property + def presence_sensor_entity_id(self) -> bool: + """Return true if the presence is configured and presence sensor is OFF""" + return self._presence_sensor_entity_id + + def __str__(self): + return f"PresenceManager-{self.name}" diff --git a/custom_components/versatile_thermostat/feature_repair_incorrect_state_manager.py b/custom_components/versatile_thermostat/feature_repair_incorrect_state_manager.py new file mode 100644 index 0000000..e117a4e --- /dev/null +++ b/custom_components/versatile_thermostat/feature_repair_incorrect_state_manager.py @@ -0,0 +1,170 @@ +# pylint: disable=line-too-long + +"""Implements the Repair Incorrect State feature as a Feature Manager""" + +from typing import Any +from datetime import datetime, timezone + +from homeassistant.core import HomeAssistant + +from .const import ( + CONF_REPAIR_INCORRECT_STATE, + DEFAULT_REPAIR_INCORRECT_STATE, + REPAIR_MAX_ATTEMPTS, + REPAIR_MIN_DELAY_AFTER_INIT_SEC, + overrides, +) +from vtherm_api.log_collector import get_vtherm_logger +from .commons_type import ConfigData +from .base_manager import BaseFeatureManager + +_LOGGER = get_vtherm_logger(__name__) + + +class FeatureRepairIncorrectStateManager(BaseFeatureManager): + """Detects and repairs discrepancies between VTherm's desired state + and the actual state of underlying entities. + + On each control heating cycle, if the feature is enabled, it compares + the desired state (should_device_be_active) with the actual state + (is_device_active) for each underlying entity. If they differ, it + re-emits the desired command. The number of consecutive repairs is + capped at REPAIR_MAX_ATTEMPTS to prevent infinite loops. + + The feature only activates at least REPAIR_MIN_DELAY_AFTER_INIT_SEC + seconds after VTherm has become fully operational (is_ready). + """ + + unrecorded_attributes = frozenset( + { + "is_repair_incorrect_state_configured", + } + ) + + def __init__(self, vtherm: Any, hass: HomeAssistant): + """Init of a FeatureManager""" + super().__init__(vtherm, hass) + + self._is_configured: bool = False + self._ready_start_time: datetime | None = None + self._consecutive_repair_count: int = 0 + + @overrides + def post_init(self, entry_infos: ConfigData): + """Reinit of the manager""" + self._is_configured = entry_infos.get( + CONF_REPAIR_INCORRECT_STATE, DEFAULT_REPAIR_INCORRECT_STATE + ) + self._ready_start_time = None + self._consecutive_repair_count = 0 + + @overrides + async def start_listening(self): + """Start listening - no external entity to monitor for this feature""" + + @overrides + def stop_listening(self): + """Stop listening - nothing to clean up for this feature""" + + @property + @overrides + def is_configured(self) -> bool: + """True if the feature is enabled""" + return self._is_configured + + async def check_and_repair(self) -> bool: + """Check all underlyings for state discrepancies and repair if needed. + + Called on each control heating cycle. + Returns True if at least one repair was performed, False otherwise. + """ + if not self._is_configured: + return False + + if not self._vtherm.is_ready: + # Reset so the delay restarts if VTherm loses readiness + self._ready_start_time = None + return False + + now = datetime.now(timezone.utc) + + # Record the first time VTherm becomes ready + if self._ready_start_time is None: + self._ready_start_time = now + _LOGGER.debug( + "%s - RepairIncorrectStateManager: VTherm just became ready, " + "waiting %ds before activating", + self._vtherm.name, + REPAIR_MIN_DELAY_AFTER_INIT_SEC, + ) + return False + + # Wait for the minimum delay after init + elapsed = (now - self._ready_start_time).total_seconds() + if elapsed < REPAIR_MIN_DELAY_AFTER_INIT_SEC: + return False + + # Stop if the maximum consecutive repair count is reached + if self._consecutive_repair_count >= REPAIR_MAX_ATTEMPTS: + _LOGGER.error( + "%s - RepairIncorrectStateManager: maximum repair attempts (%d) " "reached. Stopped attempting repairs to avoid infinite loop.", + self._vtherm.name, + REPAIR_MAX_ATTEMPTS, + ) + self._consecutive_repair_count += 1 + if self._consecutive_repair_count >= 2 * REPAIR_MAX_ATTEMPTS: + _LOGGER.info( + "%s - RepairIncorrectStateManager: consecutive repair count has doubled the max attempts, resetting the counter to allow new repair attempts.", + self._vtherm.name, + ) + self._consecutive_repair_count = 0 + else: + return False + + repaired = False + # build a list of underlying to repair as the vtherm.underlyings concatened to the list of vtherm.underlyings_valve_regulation if it exists + for underlying in self._vtherm.all_underlying_entities: + _LOGGER.debug("%s - RepairIncorrectStateManager: checking underlying %s for state discrepancies", self, underlying.entity_id) + repaired_this = await underlying.check_and_repair() + if repaired_this: + _LOGGER.warning( + "%s - RepairIncorrectStateManager: underlying %s was repaired. Consecutive repairs so far: %d", + self._vtherm.name, + underlying.entity_id, + self._consecutive_repair_count + 1, + ) + repaired = True + + if repaired: + self._consecutive_repair_count += 1 + else: + self._consecutive_repair_count = 0 + + return repaired + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): + """Add custom attributes for diagnostics""" + extra_state_attributes.update( + { + "is_repair_incorrect_state_configured": self._is_configured, + } + ) + + if self._is_configured: + extra_state_attributes.update( + { + "repair_incorrect_state_manager": { + "consecutive_repair_count": self._consecutive_repair_count, + "max_attempts": REPAIR_MAX_ATTEMPTS, + "min_delay_after_init_sec": REPAIR_MIN_DELAY_AFTER_INIT_SEC, + } + } + ) + + @property + def is_detected(self) -> bool: + """Return the overall state of the feature manager based on repair attempts""" + return self._consecutive_repair_count > 0 + + def __str__(self): + return f"RepairIncorrectStateManager-{self.name}" diff --git a/custom_components/versatile_thermostat/feature_safety_manager.py b/custom_components/versatile_thermostat/feature_safety_manager.py new file mode 100644 index 0000000..f3688e3 --- /dev/null +++ b/custom_components/versatile_thermostat/feature_safety_manager.py @@ -0,0 +1,322 @@ +# pylint: disable=line-too-long + +""" Implements the Safety as a Feature Manager""" + +from typing import Any +from homeassistant.const import ( + STATE_ON, + STATE_OFF, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) + +from homeassistant.core import HomeAssistant + +from homeassistant.components.climate import HVACAction + + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons import write_event_log +from .commons_type import ConfigData + +from .base_manager import BaseFeatureManager +from .vtherm_central_api import VersatileThermostatAPI +from .vtherm_hvac_mode import VThermHvacMode +from vtherm_api.log_collector import get_vtherm_logger + +_LOGGER = get_vtherm_logger(__name__) + + +class FeatureSafetyManager(BaseFeatureManager): + """The implementation of the Safety feature""" + + unrecorded_attributes = frozenset( + { + "safety_delay_min", + "safety_min_on_percent", + "safety_default_on_percent", + "is_safety_configured", + } + ) + + def __init__(self, vtherm: Any, hass: HomeAssistant): + """Init of a featureManager""" + super().__init__(vtherm, hass) + + self._is_configured: bool = False + self._safety_delay_min = None + self._safety_min_on_percent = None + self._safety_default_on_percent = None + self._safety_state = STATE_UNAVAILABLE + self._is_outdoor_checked = True + + @overrides + def post_init(self, entry_infos: ConfigData): + """Reinit of the manager""" + self._safety_delay_min = entry_infos.get(CONF_SAFETY_DELAY_MIN) + self._safety_min_on_percent = ( + entry_infos.get(CONF_SAFETY_MIN_ON_PERCENT) + if entry_infos.get(CONF_SAFETY_MIN_ON_PERCENT) is not None + else DEFAULT_SAFETY_MIN_ON_PERCENT + ) + self._safety_default_on_percent = ( + entry_infos.get(CONF_SAFETY_DEFAULT_ON_PERCENT) + if entry_infos.get(CONF_SAFETY_DEFAULT_ON_PERCENT) is not None + else DEFAULT_SAFETY_DEFAULT_ON_PERCENT + ) + + if ( + self._safety_delay_min is not None + and self._safety_default_on_percent is not None + and self._safety_default_on_percent is not None + ): + self._safety_state = STATE_UNKNOWN + self._is_configured = True + + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass) + self._is_outdoor_checked = not api.safety_mode or api.safety_mode.get("check_outdoor_sensor") is not False + + _LOGGER.info("%s - is_outdoor_checked is %s", self, self._is_outdoor_checked) + + @overrides + async def start_listening(self): + """Start listening the underlying entity""" + + @overrides + def stop_listening(self): + """Stop listening and remove the eventual timer still running""" + + @overrides + async def refresh_state(self) -> bool: + """Check the safety and an eventual action + Return True is safety should be active""" + + if not self._is_configured: + _LOGGER.debug("%s - safety is disabled (or not configured)", self) + return False + + if self._vtherm.requested_state.hvac_mode == VThermHvacMode_OFF: + self._safety_state = STATE_OFF + _LOGGER.debug("%s - safety is OFF because requested_state is OFF", self) + return False + + now = self._vtherm.now + current_tz = dt_util.get_time_zone(self._hass.config.time_zone) + + is_safety_detected = self.is_safety_detected + + delta_temp = ( + now - self._vtherm.last_temperature_measure.replace(tzinfo=current_tz) + ).total_seconds() / 60.0 + delta_ext_temp = ( + now - self._vtherm.last_ext_temperature_measure.replace(tzinfo=current_tz) + ).total_seconds() / 60.0 + + mode_cond = self._vtherm.hvac_mode != VThermHvacMode_OFF + + temp_cond: bool = delta_temp > self._safety_delay_min or (self._is_outdoor_checked and delta_ext_temp > self._safety_delay_min) + climate_cond: bool = ( + self._vtherm.is_over_climate + and self._vtherm.hvac_action + not in [ + HVACAction.COOLING, + HVACAction.IDLE, + ] + ) + switch_cond: bool = ( + not self._vtherm.is_over_climate + and self._vtherm.has_prop + and self._vtherm.proportional_algorithm is not None + and self._vtherm.proportional_algorithm.calculated_on_percent + >= self._safety_min_on_percent + ) + + _LOGGER.debug( + "%s - checking safety delta_temp=%.1f delta_ext_temp=%.1f mod_cond=%s temp_cond=%s climate_cond=%s switch_cond=%s", + self, + delta_temp, + delta_ext_temp, + mode_cond, + temp_cond, + climate_cond, + switch_cond, + ) + + # Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in safety ! + should_climate_be_in_safety = False # temp_cond and climate_cond + should_switch_be_in_safety = temp_cond and switch_cond + should_be_in_safety = should_climate_be_in_safety or should_switch_be_in_safety + + should_start_safety = mode_cond and not is_safety_detected and should_be_in_safety + # attr_preset_mode is not necessary normaly. It is just here to be sure + should_stop_safety = is_safety_detected and not should_be_in_safety + + # Logging and event + if should_start_safety: + if should_climate_be_in_safety: + _LOGGER.warning( + "%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Setting it into safety mode", + self, + self._safety_delay_min, + delta_temp, + delta_ext_temp, + self._vtherm.hvac_action, + ) + elif should_switch_be_in_safety: + _LOGGER.warning( + "%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f %%) is over defined value (%.2f %%). Set it into safety mode", + self, + self._safety_delay_min, + delta_temp, + delta_ext_temp, + self._vtherm.proportional_algorithm.on_percent * 100, + self._safety_min_on_percent * 100 if self._safety_min_on_percent is not None else 0, + ) + + self._vtherm.send_event( + EventType.TEMPERATURE_EVENT, + { + "last_temperature_measure": self._vtherm.last_temperature_measure.replace( + tzinfo=current_tz + ).isoformat(), + "last_ext_temperature_measure": self._vtherm.last_ext_temperature_measure.replace( + tzinfo=current_tz + ).isoformat(), + "current_temp": self._vtherm.current_temperature, + "current_ext_temp": self._vtherm.current_outdoor_temperature, + "target_temp": self._vtherm.target_temperature, + }, + ) + + # Start safety mode + if should_start_safety: + write_event_log(_LOGGER, self._vtherm, "Starting safety mode") + self._safety_state = STATE_ON + # self._vtherm.save_hvac_mode() + # self._vtherm.save_preset_mode() + if self._vtherm.has_prop: + self._vtherm.set_safety(self._safety_default_on_percent) + + self._vtherm.send_event( + EventType.SAFETY_EVENT, + { + "type": "start", + "last_temperature_measure": self._vtherm.last_temperature_measure.replace(tzinfo=current_tz).isoformat(), + "last_ext_temperature_measure": self._vtherm.last_ext_temperature_measure.replace(tzinfo=current_tz).isoformat(), + "current_temp": self._vtherm.current_temperature, + "current_ext_temp": self._vtherm.current_outdoor_temperature, + "target_temp": self._vtherm.target_temperature, + }, + ) + + # Stop safety mode + elif should_stop_safety: + write_event_log(_LOGGER, self._vtherm, "Ending safety mode") + _LOGGER.warning("%s - End of safety mode.", self) + self._safety_state = STATE_OFF + if self._vtherm.has_prop: + self._vtherm.unset_safety() + self._vtherm.send_event( + EventType.SAFETY_EVENT, + { + "type": "end", + "last_temperature_measure": self._vtherm.last_temperature_measure.replace(tzinfo=current_tz).isoformat(), + "last_ext_temperature_measure": self._vtherm.last_ext_temperature_measure.replace(tzinfo=current_tz).isoformat(), + "current_temp": self._vtherm.current_temperature, + "current_ext_temp": self._vtherm.current_outdoor_temperature, + "target_temp": self._vtherm.target_temperature, + }, + ) + + # Initialize the safety_state if not already done + elif not should_be_in_safety and self._safety_state in [STATE_UNKNOWN]: + self._safety_state = STATE_OFF + + return self._safety_state == STATE_ON + + async def _async_update_states_later(self, _=None): + """Called at next tick to update states without recursion""" + self._vtherm.requested_state.force_changed() + await self._vtherm.update_states(force=True) + + async def refresh_and_update_if_changed(self) -> bool: + """Refresh the safety state and update_states of VTherm if changed + Returns True if the state has changed, False otherwise""" + old_safety: bool = self.is_safety_detected + if old_safety != await self.refresh_state(): + # issue 1450 - schedule update_states at next tick to avoid recursion + self._hass.async_create_task(self._async_update_states_later()) + return True + + return False + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): + """Add some custom attributes""" + + extra_state_attributes.update( + { + "is_safety_configured": self._is_configured, + } + ) + + if self._is_configured: + extra_state_attributes.update( + { + "safety_manager": { + "safety_state": self._safety_state, + "safety_delay_min": self._safety_delay_min, + "safety_min_on_percent": self._safety_min_on_percent, + "safety_default_on_percent": self._safety_default_on_percent, + } + } + ) + + @property + def is_configured(self) -> bool: + """Return True of the safety feature is configured""" + return self._is_configured + + def set_safety_delay_min(self, safety_delay_min): + """Set the delay min""" + self._safety_delay_min = safety_delay_min + + def set_safety_min_on_percent(self, safety_min_on_percent): + """Set the min on percent""" + self._safety_min_on_percent = safety_min_on_percent + + def set_safety_default_on_percent(self, safety_default_on_percent): + """Set the default on_percent""" + self._safety_default_on_percent = safety_default_on_percent + + @property + def is_safety_detected(self) -> bool: + """Returns the is vtherm is in safety mode""" + return self._safety_state == STATE_ON + + @property + def safety_state(self) -> str: + """Returns the safety state: STATE_ON, STATE_OFF, STATE_UNKWNON, STATE_UNAVAILABLE""" + return self._safety_state + + @property + def safety_delay_min(self) -> bool: + """Returns the safety delay min""" + return self._safety_delay_min + + @property + def safety_min_on_percent(self) -> bool: + """Returns the safety min on percent""" + return self._safety_min_on_percent + + @property + def safety_default_on_percent(self) -> bool: + """Returns the safety safety_default_on_percent""" + return self._safety_default_on_percent + + @property + def is_detected(self) -> bool: + """Return the overall state of the feature manager based on safety states""" + return self.is_safety_detected + + def __str__(self): + return f"SafetyManager-{self.name}" diff --git a/custom_components/versatile_thermostat/feature_timed_preset_manager.py b/custom_components/versatile_thermostat/feature_timed_preset_manager.py new file mode 100644 index 0000000..fff93d2 --- /dev/null +++ b/custom_components/versatile_thermostat/feature_timed_preset_manager.py @@ -0,0 +1,316 @@ +""" Implements the Timed Preset Feature Manager """ + +# pylint: disable=line-too-long + +from datetime import datetime, timedelta +from typing import Any + +from homeassistant.core import ( + HomeAssistant, + callback, +) +from homeassistant.helpers.event import async_track_point_in_time + +from vtherm_api.log_collector import get_vtherm_logger +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons_type import ConfigData + +from .commons import write_event_log + +from .base_manager import BaseFeatureManager +from .vtherm_preset import VThermPreset + + +_LOGGER = get_vtherm_logger(__name__) + + +class FeatureTimedPresetManager(BaseFeatureManager): + """The implementation of the TimedPreset feature. + + This feature allows forcing a preset for a given duration. + When the duration expires, the original preset (from requested_state) is restored. + """ + + unrecorded_attributes = frozenset( + { + "timed_preset_manager", + } + ) + + def __init__(self, vtherm: Any, hass: HomeAssistant): + """Init of a featureManager""" + super().__init__(vtherm, hass) + + self._is_timed_preset_active: bool = False + self._timed_preset: VThermPreset | None = None + self._original_preset: VThermPreset | None = None + self._timed_preset_end_time: datetime | None = None + self._cancel_timer: Any | None = None + + @overrides + def post_init(self, entry_infos: ConfigData): + """Reinit of the manager""" + # The timed preset feature is always available, no configuration needed + pass + + @overrides + async def start_listening(self): + """Start listening - nothing to listen to for this feature""" + pass + + @overrides + def stop_listening(self): + """Stop listening and remove the eventual timer still running""" + self._cancel_timed_preset_timer() + super().stop_listening() + + @callback + def restore_state(self, old_state: Any): + """Implement the restoration hook to re-populate timed presets after restart.""" + + # 1. Retrieve the persistence dictionary from attributes + # Matches the key used in add_custom_attributes + manager_attr = old_state.attributes.get("timed_preset_manager") + if not manager_attr or not manager_attr.get("is_active"): + return + + end_time_str = manager_attr.get("end_time") + preset_str = manager_attr.get("preset") + original_preset_str = manager_attr.get("original_preset") + + if not end_time_str or not preset_str: + return + + try: + # 2. Re-parse the end_time and preset mode + end_time = datetime.fromisoformat(end_time_str) + now = self._vtherm.now + + # 3. Re-populate internal manager variables + self._is_timed_preset_active = True + self._timed_preset = VThermPreset(preset_str) + self._original_preset = VThermPreset(original_preset_str) if original_preset_str else None + self._timed_preset_end_time = end_time + + # 4. Reschedule the expiration task if time remains + if end_time > now: + # While the timed preset is active, requested_state must carry it so + # other managers like auto start/stop recalculate from the forced preset. + self._vtherm.requested_state.set_preset(self._timed_preset) + _LOGGER.info("%s - Resuming timed preset %s. Reverting at %s", self, preset_str, end_time) + self._cancel_timer = async_track_point_in_time( + self._hass, + self._async_timed_preset_expired, + self._timed_preset_end_time, + ) + else: + _LOGGER.info("%s - Timed preset expired during downtime. Cleanup will follow.", self) + # The existing safety check in refresh_state() will handle the cleanup + # and revert to requested_state during the startup cycle. + except (ValueError, TypeError) as err: + _LOGGER.error("%s - Failed to restore timed preset state: %s", self, err) + + @overrides + async def refresh_state(self) -> bool: + """Check if the timed preset is still active. + Return True if timed preset is active""" + + if not self._is_timed_preset_active: + return False + + # Check if the timer has expired (safety check in case timer callback failed) + if self._timed_preset_end_time and self._vtherm.now >= self._timed_preset_end_time: + _LOGGER.debug("%s - timed preset has expired (safety check)", self) + await self._end_timed_preset() + return False + + return self._is_timed_preset_active + + async def set_timed_preset(self, preset: VThermPreset, duration_minutes: float) -> bool: + """Set a preset for a given duration in minutes. + + Args: + preset: The preset to apply temporarily + duration_minutes: The duration in minutes + + Returns: + True if the timed preset was set successfully, False otherwise + """ + if duration_minutes <= 0: + _LOGGER.warning("%s - duration must be positive, got %s", self, duration_minutes) + return False + + if preset not in self._vtherm.vtherm_preset_modes and preset not in [VThermPreset.NONE]: + _LOGGER.warning("%s - preset %s is not available for this thermostat", self, preset) + return False + + # Cancel any existing timer + self._cancel_timed_preset_timer() + + # Capture the original preset before overriding it + if not self._is_timed_preset_active: + self._original_preset = self._vtherm.requested_state.preset + + # Store the timed preset information + self._timed_preset = preset + self._timed_preset_end_time = self._vtherm.now + timedelta(minutes=duration_minutes) + self._is_timed_preset_active = True + + # Keep requested_state aligned with the timed preset while it is active so + # state recalculation uses the forced preset even if HVAC is currently off. + self._vtherm.requested_state.set_preset(preset) + + # Schedule the end of timed preset + self._cancel_timer = async_track_point_in_time( + self._hass, + self._async_timed_preset_expired, + self._timed_preset_end_time, + ) + + write_event_log( + _LOGGER, + self._vtherm, + f"Timed preset started: {preset} for {duration_minutes} minutes until {self._timed_preset_end_time}", + ) + + # Send an event + self._vtherm.send_event( + event_type=EventType.TIMED_PRESET_EVENT, + data={ + "type": "start", + "name": self.name, + "preset": str(preset), + "duration_minutes": duration_minutes, + "end_time": self._timed_preset_end_time.isoformat(), + "original_preset": str(self._original_preset), + }, + ) + + # Force update of the thermostat state + self._vtherm.requested_state.force_changed() + await self._vtherm.update_states(force=True) + + self._vtherm.update_custom_attributes() + + return True + + async def cancel_timed_preset(self) -> bool: + """Cancel the current timed preset if active. + + Returns: + True if a timed preset was cancelled, False if none was active + """ + if not self._is_timed_preset_active: + return False + + await self._end_timed_preset(cancelled=True) + return True + + @callback + async def _async_timed_preset_expired(self, _: datetime): + """Called when the timed preset timer expires.""" + _LOGGER.debug("%s - timed preset timer expired", self) + await self._end_timed_preset() + + async def _end_timed_preset(self, cancelled: bool = False): + """End the timed preset and restore the original preset.""" + if not self._is_timed_preset_active: + return + + old_preset = self._timed_preset + + # Cancel the timer if still active + self._cancel_timed_preset_timer() + + # Restore the original preset explicitly (handles the post-restart case) + if self._original_preset is not None: + self._vtherm.requested_state.set_preset(self._original_preset) + + # Reset state + self._is_timed_preset_active = False + self._timed_preset = None + self._original_preset = None + self._timed_preset_end_time = None + + write_event_log( + _LOGGER, + self._vtherm, + f"Timed preset ended: {old_preset} {'(cancelled)' if cancelled else '(expired)'}", + ) + + # Send an event + self._vtherm.send_event( + event_type=EventType.TIMED_PRESET_EVENT, + data={ + "type": "end", + "name": self.name, + "preset": str(old_preset), + "cause": "cancelled" if cancelled else "expired", + "restored_preset": str(self._vtherm.requested_state.preset), + }, + ) + + # Force update of the thermostat state to restore original preset + self._vtherm.requested_state.force_changed() + await self._vtherm.update_states(force=True) + + self._vtherm.update_custom_attributes() + + def _cancel_timed_preset_timer(self): + """Cancel the timed preset timer if active.""" + if self._cancel_timer: + self._cancel_timer() + self._cancel_timer = None + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): + """Add some custom attributes""" + extra_state_attributes.update( + { + "timed_preset_manager": { + "is_active": self._is_timed_preset_active, + "preset": str(self._timed_preset) if self._timed_preset else None, + "original_preset": str(self._original_preset) if self._original_preset else None, + "end_time": self._timed_preset_end_time.isoformat() if self._timed_preset_end_time else None, + "remaining_time_min": self.remaining_time_min, + } + } + ) + + @property + def remaining_time_min(self) -> int: + """Return the remaining time in minutes, or 0 if not active or expired.""" + if not self._is_timed_preset_active or not self._timed_preset_end_time: + return 0 + + remaining = self._timed_preset_end_time - self._vtherm.now + remaining_minutes = remaining.total_seconds() / 60 + return max(0, round(remaining_minutes)) + + @property + def is_timed_preset_active(self) -> bool: + """Return True if a timed preset is currently active.""" + return self._is_timed_preset_active + + @property + def timed_preset(self) -> VThermPreset | None: + """Return the current timed preset, or None if not active.""" + return self._timed_preset if self._is_timed_preset_active else None + + @property + def timed_preset_end_time(self) -> datetime | None: + """Return the end time of the timed preset, or None if not active.""" + return self._timed_preset_end_time if self._is_timed_preset_active else None + + @overrides + @property + def is_configured(self) -> bool: + """Return True - timed preset feature is always available.""" + return True + + @property + def is_detected(self) -> bool: + """Return the overall state of the feature manager based on timed preset states""" + return self.is_timed_preset_active + + def __str__(self): + return f"TimedPresetManager-{self.name}" diff --git a/custom_components/versatile_thermostat/feature_window_manager.py b/custom_components/versatile_thermostat/feature_window_manager.py new file mode 100644 index 0000000..d0e63a4 --- /dev/null +++ b/custom_components/versatile_thermostat/feature_window_manager.py @@ -0,0 +1,485 @@ +""" Implements the Window Feature Manager """ + +# pylint: disable=line-too-long + +from typing import Any +from datetime import timedelta + +from homeassistant.const import ( + STATE_ON, + STATE_OFF, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import ( + HomeAssistant, + callback, + Event, +) +from homeassistant.helpers.event import ( + async_track_state_change_event, + EventStateChangedData, + async_call_later, +) + + +from homeassistant.exceptions import ConditionError +from homeassistant.helpers import condition + +from vtherm_api.log_collector import get_vtherm_logger +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons import write_event_log +from .commons_type import ConfigData +from .vtherm_hvac_mode import VThermHvacMode + +from .base_manager import BaseFeatureManager +from .open_window_algorithm import WindowOpenDetectionAlgorithm + +_LOGGER = get_vtherm_logger(__name__) + + +class FeatureWindowManager(BaseFeatureManager): + """The implementation of the Window feature""" + + unrecorded_attributes = frozenset( + { + "window_sensor_entity_id", + "is_window_configured", + "window_delay_sec", + "window_off_delay_sec", + "window_auto_configured", + "window_auto_open_threshold", + "window_auto_close_threshold", + "window_auto_max_duration", + "window_action", + } + ) + + def __init__(self, vtherm: Any, hass: HomeAssistant): + """Init of a featureManager""" + super().__init__(vtherm, hass) + self._window_sensor_entity_id: str = None + self._window_state: str = STATE_UNAVAILABLE + self._window_auto_open_threshold: float = 0 + self._window_auto_close_threshold: float = 0 + self._window_auto_max_duration: int = 0 + self._window_auto_state: bool = False + self._window_auto_algo: WindowOpenDetectionAlgorithm = None + self._is_window_bypass: bool = False + self._window_action: str = None + self._window_delay_sec: int | None = 0 + self._window_off_delay_sec: int | None = 0 + self._is_configured: bool = False + self._is_window_auto_configured: bool = False + self._window_call_cancel: callable = None + + @overrides + def post_init(self, entry_infos: ConfigData): + """Reinit of the manager""" + self.dearm_window_timer() + + self._window_auto_state = STATE_UNAVAILABLE + self._window_state = STATE_UNAVAILABLE + + self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR) + self._window_delay_sec = entry_infos.get(CONF_WINDOW_DELAY) + # default is the WINDOW_ON delay if not configured + self._window_off_delay_sec = entry_infos.get(CONF_WINDOW_OFF_DELAY, self._window_delay_sec) + + self._window_action = ( + entry_infos.get(CONF_WINDOW_ACTION) or CONF_WINDOW_TURN_OFF + ) + + 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) + + use_window_feature = entry_infos.get(CONF_USE_WINDOW_FEATURE, False) + + if ( # pylint: disable=too-many-boolean-expressions + use_window_feature + and self._window_sensor_entity_id is None + and 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 + and self._window_action is not None + ): + self._is_window_auto_configured = True + self._window_auto_state = STATE_UNKNOWN + + self._window_auto_algo = WindowOpenDetectionAlgorithm( + alert_threshold=self._window_auto_open_threshold, + end_alert_threshold=self._window_auto_close_threshold, + vtherm=self._vtherm, + ) + + if self._is_window_auto_configured or ( + use_window_feature + and self._window_sensor_entity_id is not None + and self._window_delay_sec is not None + and self._window_action is not None + ): + self._is_configured = True + self._window_state = STATE_UNKNOWN + + @overrides + async def start_listening(self): + """Start listening the underlying entity""" + + # Try to get last window bypass state + old_state = await self._vtherm.async_get_last_state() + self._is_window_bypass = old_state is not None and hasattr(old_state, "attributes") and old_state.attributes.get("is_window_bypass") is True + + if self._is_configured: + self.stop_listening() + if self._window_sensor_entity_id: + self.add_listener( + async_track_state_change_event( + self.hass, + [self._window_sensor_entity_id], + self._window_sensor_changed, + ) + ) + + @overrides + def stop_listening(self): + """Stop listening and remove the eventual timer still running""" + self.dearm_window_timer() + super().stop_listening() + + def dearm_window_timer(self): + """Dearm the eventual motion time running""" + if self._window_call_cancel: + self._window_call_cancel() + self._window_call_cancel = None + + @overrides + async def refresh_state(self) -> bool: + """Tries to get the last state from sensor + Returns True if a change has been made""" + ret = False + if self._is_configured and self._window_sensor_entity_id is not None: + + window_state = self.hass.states.get(self._window_sensor_entity_id) + if window_state and window_state.state not in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + _LOGGER.debug( + "%s - Window state have been retrieved: %s", + self, + self._window_state, + ) + # recalculate the right target_temp in activity mode + ret = await self.update_window_state(window_state.state) + + return ret + + @callback + async def _window_sensor_changed(self, event: Event[EventStateChangedData]): + """Handle window sensor changes.""" + new_state = event.data.get("new_state") + old_state = event.data.get("old_state") + write_event_log(_LOGGER, self._vtherm, f"Window sensor changed to state {new_state.state if new_state else None}") + + # Check delay condition + async def try_window_condition(_): + try: + long_enough = condition.state( + self._hass, + self._window_sensor_entity_id, + new_state.state, + timedelta(seconds=delay), + ) + except ConditionError: + long_enough = False + + if not long_enough: + _LOGGER.debug("%s - Window delay condition is not satisfied. Ignore window event", self) + self._window_state = old_state.state or STATE_OFF + return + + _LOGGER.debug("%s - Window delay condition is satisfied", self) + + if self._window_state == new_state.state: + _LOGGER.debug("%s - no change in window state. Forget the event", self) + return + + _LOGGER.debug("%s - Window ByPass is : %s", self, self._is_window_bypass) + if self._is_window_bypass: + _LOGGER.info( + "%s - Window ByPass is activated. Ignore window event", self + ) + # We change the state but we don't apply the change + self._window_state = new_state.state + else: + await self.update_window_state(new_state.state) + + self._vtherm.update_custom_attributes() + + delay = self._window_delay_sec if new_state.state == STATE_ON else self._window_off_delay_sec + if new_state is None or old_state is None or new_state.state == old_state.state: + return try_window_condition + + self.dearm_window_timer() + self._window_call_cancel = async_call_later(self.hass, timedelta(seconds=delay), try_window_condition) + # For testing purpose we need to access the inner function + return try_window_condition + + async def update_window_state(self, new_state: str | None = None, bypass: bool = False) -> bool: + """Change the window detection state. + new_state is on if an open window have been detected or off else + return True if the state have changed + """ + + # No changes + if (old_state := self._window_state) == new_state and not bypass: + return False + + # Windows is now closed + if new_state != STATE_ON: + write_event_log(_LOGGER, self._vtherm, "Window is detected as closed.") + # Window is now opened + else: + write_event_log(_LOGGER, self._vtherm, f"Window is detected as open ({self._window_action})") + + self._window_state = new_state + if old_state != new_state: + if self._vtherm.auto_start_stop_manager: + # because window may have an impact on auto-start/stop + await self._vtherm.auto_start_stop_manager.refresh_state() + self._vtherm.requested_state.force_changed() + await self._vtherm.update_states(True) + return True + + return False + + async def manage_window_auto(self, in_cycle=False) -> callable: + """The management of the window auto feature + Returns the dearm function used to deactivate the window auto""" + + 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._vtherm.send_event( + EventType.WINDOW_AUTO_EVENT, + {"type": "end", "cause": cause, "curve_slope": slope}, + ) + # Set attributes + self._window_auto_state = STATE_OFF + await self.update_window_state(self._window_auto_state) + # await self.restore_hvac_mode(True) + + self.dearm_window_timer() + + if not self._window_auto_algo: + return None + + if in_cycle: + slope = self._window_auto_algo.check_age_last_measurement( + temperature=self._vtherm.ema_temperature, + datetime_now=self._vtherm.now, + ) + else: + slope = self._window_auto_algo.add_temp_measurement( + temperature=self._vtherm.ema_temperature, + datetime_measure=self._vtherm.last_temperature_measure, + ) + + _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.is_window_bypass or not self._is_window_auto_configured: + _LOGGER.debug( + "%s - Window auto event is ignored because bypass is ON or window auto detection is disabled", + self, + ) + return None + + if self._window_auto_algo.is_window_open_detected() and self._window_auto_state in [STATE_UNKNOWN, STATE_OFF] and self._vtherm.hvac_mode != VThermHvacMode_OFF: + if ( + self._vtherm.has_prop + and self._vtherm.proportional_algorithm # Added check to avoid initialization issues + and self._vtherm.safe_on_percent <= 0.0 + ): + _LOGGER.info( + "%s - Start auto detection of open window slope=%.3f but no heating detected (on_percent<=0). Forget the event", + self, + slope, + ) + return dearm_window_auto + + _LOGGER.warning( + "%s - Start auto detection of open window slope=%.3f", self, slope + ) + + # Send an event + self._vtherm.send_event( + EventType.WINDOW_AUTO_EVENT, + {"type": "start", "cause": "slope alert", "curve_slope": slope}, + ) + # Set attributes + self._window_auto_state = STATE_ON + await self.update_window_state(self._window_auto_state) + + # Arm the end trigger + self.dearm_window_timer() + 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 == STATE_ON + ): + await deactivate_window_auto(False) + + # For testing purpose we need to return the inner function + return dearm_window_auto + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): + """Add some custom attributes""" + extra_state_attributes.update( + { + "is_window_configured": self.is_configured, + "is_window_auto_configured": self.is_window_auto_configured, + } + ) + + if self.is_configured or self.is_window_auto_configured: + extra_state_attributes.update( + { + "window_manager": { + "window_state": self.window_state, + "window_auto_state": self.window_auto_state, + "window_action": self.window_action, + "is_window_bypass": self._is_window_bypass, + "window_sensor_entity_id": self._window_sensor_entity_id, + "window_delay_sec": self._window_delay_sec, + "window_off_delay_sec": self._window_off_delay_sec, + "window_auto_open_threshold": self._window_auto_open_threshold, + "window_auto_close_threshold": self._window_auto_close_threshold, + "window_auto_max_duration": self._window_auto_max_duration, + } + } + ) + + async def set_window_bypass(self, window_bypass: bool) -> bool: + """Set the window bypass flag + Return True if state have been changed""" + self._is_window_bypass = window_bypass + + _LOGGER.info("%s - Last window state was %s & ByPass is now %s.",self,self._window_state,self._is_window_bypass,) + self._vtherm.requested_state.force_changed() + await self._vtherm.update_states(True) + + @overrides + @property + def is_configured(self) -> bool: + """Return True of the window feature is configured""" + return self._is_configured + + @property + def is_window_auto_configured(self) -> bool: + """Return True of the window automatic detection is configured""" + return self._is_window_auto_configured + + @property + def window_state(self) -> str | None: + """Return the current window state STATE_ON or STATE_OFF + or STATE_UNAVAILABLE if not configured""" + if not self._is_configured: + return STATE_UNAVAILABLE + return self._window_state + + @property + def window_auto_state(self) -> str | None: + """Return the current window auto state STATE_ON or STATE_OFF + or STATE_UNAVAILABLE if not configured""" + if not self._is_configured: + return STATE_UNAVAILABLE + return self._window_auto_state + + @property + def is_window_bypass(self) -> str | None: + """Return True if the window bypass is activated""" + if not self._is_configured: + return False + return self._is_window_bypass + + @property + def is_window_detected(self) -> bool: + """Return true if the window is configured and open and bypass is not ON""" + return self._is_configured and ( + self._window_state == STATE_ON or self._window_auto_state == STATE_ON + ) and not self._is_window_bypass + + @property + def window_sensor_entity_id(self) -> bool: + """Return true if the presence is configured and presence sensor is OFF""" + return self._window_sensor_entity_id + + @property + def window_delay_sec(self) -> bool: + """Return the window on delay""" + return self._window_delay_sec + + @property + def window_off_delay_sec(self) -> bool: + """Return the window off delay""" + return self._window_off_delay_sec + + @property + def window_action(self) -> bool: + """Return the window action""" + return self._window_action + + @property + def window_auto_open_threshold(self) -> bool: + """Return the window_auto_open_threshold""" + return self._window_auto_open_threshold + + @property + def window_auto_close_threshold(self) -> bool: + """Return the window_auto_close_threshold""" + return self._window_auto_close_threshold + + @property + def window_auto_max_duration(self) -> bool: + """Return the window_auto_max_duration""" + return self._window_auto_max_duration + + @property + def last_slope(self) -> float: + """Return the last slope (in °C/hour)""" + if not self._window_auto_algo: + return None + return self._window_auto_algo.last_slope + + @property + def is_detected(self) -> bool: + """Return the overall state of the feature manager based on window state""" + return self.is_window_detected + + def __str__(self): + return f"WindowManager-{self.name}" diff --git a/custom_components/versatile_thermostat/icons.json b/custom_components/versatile_thermostat/icons.json new file mode 100644 index 0000000..09b5053 --- /dev/null +++ b/custom_components/versatile_thermostat/icons.json @@ -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" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/versatile_thermostat/keep_alive.py b/custom_components/versatile_thermostat/keep_alive.py new file mode 100644 index 0000000..9c64c36 --- /dev/null +++ b/custom_components/versatile_thermostat/keep_alive.py @@ -0,0 +1,139 @@ +"""Building blocks for the heater switch keep-alive feature. + +The heater switch keep-alive feature consists of regularly refreshing the state +of directly controlled switches at a configurable interval (regularly turning the +switch 'on' or 'off' again even if it is already turned 'on' or 'off'), just like +the keep_alive setting of Home Assistant's Generic Thermostat integration: + https://www.home-assistant.io/integrations/generic_thermostat/ +""" + +import logging +from vtherm_api.log_collector import get_vtherm_logger +from collections.abc import Awaitable, Callable +from datetime import timedelta, datetime +from time import monotonic + +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers.event import async_track_time_interval + + +_LOGGER = get_vtherm_logger(__name__) + + +class BackoffTimer: + """Exponential backoff timer with a non-blocking polling-style implementation. + + Usage example: + timer = BackoffTimer(multiplier=1.5, upper_limit_sec=600) + while some_condition: + if timer.is_ready(): + do_something() + """ + + def __init__( + self, + *, + multiplier=2.0, + lower_limit_sec=30, + upper_limit_sec=86400, + initially_ready=True, + ): + """Initialize a BackoffTimer instance. + + Args: + multiplier (int, optional): Period multiplier applied when is_ready() is True. + lower_limit_sec (int, optional): Initial backoff period in seconds. + upper_limit_sec (int, optional): Maximum backoff period in seconds. + initially_ready (bool, optional): Whether is_ready() should return True the + first time it is called, or after a call to reset(). + """ + self._multiplier = multiplier + self._lower_limit_sec = lower_limit_sec + self._upper_limit_sec = upper_limit_sec + self._initially_ready = initially_ready + + self._timestamp = 0 + self._period_sec = self._lower_limit_sec + + @property + def in_progress(self) -> bool: + """Whether the backoff timer is in progress (True after a call to is_ready()).""" + return bool(self._timestamp) + + def reset(self): + """Reset a BackoffTimer instance.""" + self._timestamp = 0 + self._period_sec = self._lower_limit_sec + + def is_ready(self) -> bool: + """Check whether an exponentially increasing period of time has passed. + + Whenever is_ready() returns True, the timer period is multiplied so that + it takes longer until is_ready() returns True again. + Returns: + bool: True if enough time has passed since one of the following events, + in relation to an instance of this class: + - The last time when this method returned True, if it ever did. + - Or else, when this method was first called after a call to reset(). + - Or else, when this method was first called. + False otherwise. + """ + now = monotonic() + if self._timestamp == 0: + self._timestamp = now + return self._initially_ready + elif now - self._timestamp >= self._period_sec: + self._timestamp = now + self._period_sec = max( + self._lower_limit_sec, + min(self._upper_limit_sec, self._period_sec * self._multiplier), + ) + return True + + return False + + +class IntervalCaller: + """Repeatedly call a given async action function at a given regular interval. + + Convenience wrapper around Home Assistant's `async_track_time_interval` function. + """ + + def __init__(self, hass: HomeAssistant, interval_sec: float) -> None: + self._hass = hass + self._interval_sec = interval_sec + self._remove_handle: CALLBACK_TYPE | None = None + self.backoff_timer = BackoffTimer() + + @property + def interval_sec(self) -> float: + """Return the calling interval in seconds.""" + return self._interval_sec + + def cancel(self): + """Cancel the regular calls to the action function.""" + if self._remove_handle: + self._remove_handle() + self._remove_handle = None + + def set_async_action(self, action: Callable[[], Awaitable[None]]): + """Set the async action function to be called at regular intervals.""" + if not self._interval_sec: + return + self.cancel() + + async def callback(_time: datetime): + try: + _LOGGER.debug( + "IntervalCaller - Calling keep-alive action '%s' (%ss interval)", + action.__name__, + self._interval_sec, + ) + await action() + except Exception as e: # pylint: disable=broad-exception-caught + _LOGGER.error(e) + self.cancel() + + self._remove_handle = async_track_time_interval( + self._hass, callback, timedelta(seconds=self._interval_sec) + ) diff --git a/custom_components/versatile_thermostat/manifest.json b/custom_components/versatile_thermostat/manifest.json new file mode 100644 index 0000000..affa825 --- /dev/null +++ b/custom_components/versatile_thermostat/manifest.json @@ -0,0 +1,27 @@ +{ + "domain": "versatile_thermostat", + "name": "Versatile Thermostat", + "after_dependencies": [ + "recorder", + "http" + ], + "codeowners": [ + "@jmcollin78" + ], + "config_flow": true, + "dependencies": [], + "documentation": "https://github.com/jmcollin78/versatile_thermostat", + "homekit": {}, + "integration_type": "device", + "iot_class": "calculated", + "issue_tracker": "https://github.com/jmcollin78/versatile_thermostat/issues", + "quality_scale": "silver", + "requirements": [ + "numpy", + "scipy", + "vtherm_api>=0.3.0" + ], + "ssdp": [], + "version": "10.0.2", + "zeroconf": [] +} diff --git a/custom_components/versatile_thermostat/number.py b/custom_components/versatile_thermostat/number.py new file mode 100644 index 0000000..8afe02d --- /dev/null +++ b/custom_components/versatile_thermostat/number.py @@ -0,0 +1,560 @@ +# pylint: disable=unused-argument + +""" Implements the VersatileThermostat select component """ +import logging +from vtherm_api.log_collector import get_vtherm_logger + +# from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant, CoreState # , callback + +from homeassistant.components.number import ( + NumberEntity, + NumberMode, + NumberDeviceClass, + DOMAIN as NUMBER_DOMAIN, + DEFAULT_MAX_VALUE, + DEFAULT_MIN_VALUE, + DEFAULT_STEP, +) +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from .vtherm_central_api import VersatileThermostatAPI +from .base_entity import VersatileThermostatBaseEntity + +from .const import ( + DOMAIN, + DEVICE_MANUFACTURER, + CONF_NAME, + CONF_THERMOSTAT_TYPE, + CONF_THERMOSTAT_CENTRAL_CONFIG, + CONF_TEMP_MIN, + CONF_TEMP_MAX, + CONF_STEP_TEMPERATURE, + CONF_AC_MODE, + CONF_PRESETS_VALUES, + CONF_PRESETS_WITH_AC_VALUES, + CONF_PRESETS_AWAY_VALUES, + CONF_PRESETS_AWAY_WITH_AC_VALUES, + CONF_USE_PRESETS_CENTRAL_CONFIG, + CONF_USE_PRESENCE_FEATURE, + CONF_USE_CENTRAL_BOILER_FEATURE, + overrides, + CONF_USE_MAIN_CENTRAL_CONFIG, +) + +from .vtherm_preset import VThermPreset, VThermPresetWithAC, VThermPresetWithAway, VThermPresetWithACAway, PRESET_TEMP_SUFFIX, PRESET_AWAY_SUFFIX + +PRESET_ICON_MAPPING = { + VThermPreset.FROST + PRESET_TEMP_SUFFIX: "mdi:snowflake-thermometer", + VThermPreset.ECO + PRESET_TEMP_SUFFIX: "mdi:leaf", + VThermPreset.COMFORT + PRESET_TEMP_SUFFIX: "mdi:sofa", + VThermPreset.BOOST + PRESET_TEMP_SUFFIX: "mdi:rocket-launch", + VThermPresetWithAC.ECO + PRESET_TEMP_SUFFIX: "mdi:leaf-circle-outline", + VThermPresetWithAC.COMFORT + PRESET_TEMP_SUFFIX: "mdi:sofa-outline", + VThermPresetWithAC.BOOST + PRESET_TEMP_SUFFIX: "mdi:rocket-launch-outline", + VThermPresetWithAway.FROST + PRESET_TEMP_SUFFIX: "mdi:snowflake-thermometer", + VThermPresetWithAway.ECO + PRESET_TEMP_SUFFIX: "mdi:leaf", + VThermPresetWithAway.COMFORT + PRESET_TEMP_SUFFIX: "mdi:sofa", + VThermPresetWithAway.BOOST + PRESET_TEMP_SUFFIX: "mdi:rocket-launch", + VThermPresetWithACAway.ECO + PRESET_TEMP_SUFFIX: "mdi:leaf-circle-outline", + VThermPresetWithACAway.COMFORT + PRESET_TEMP_SUFFIX: "mdi:sofa-outline", + VThermPresetWithACAway.BOOST + PRESET_TEMP_SUFFIX: "mdi:rocket-launch-outline", +} + +_LOGGER = get_vtherm_logger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the VersatileThermostat selects with config flow.""" + unique_id = entry.entry_id + name = entry.data.get(CONF_NAME) + _LOGGER.debug("%s - Calling async_setup_entry entry=%s, data=%s", name, entry.entry_id, entry.data) + + vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) + # is_central_boiler = entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE) + + entities = [] + + if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG: + # Creates non central temperature entities + if not entry.data.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False): + if entry.data.get(CONF_AC_MODE, False): + for preset in CONF_PRESETS_WITH_AC_VALUES: + _LOGGER.debug( + "%s - configuring Number non central, AC, non AWAY for preset %s", + name, + preset, + ) + entities.append( + TemperatureNumber( + hass, unique_id, name, preset, True, False, entry.data + ) + ) + else: + for preset in CONF_PRESETS_VALUES: + _LOGGER.debug( + "%s - configuring Number non central, non AC, non AWAY for preset %s", + name, + preset, + ) + entities.append( + TemperatureNumber( + hass, unique_id, name, preset, False, False, entry.data + ) + ) + + if entry.data.get(CONF_USE_PRESENCE_FEATURE, False) is True: + if entry.data.get(CONF_AC_MODE, False): + for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES: + _LOGGER.debug( + "%s - configuring Number non central, AC, AWAY for preset %s", + name, + preset, + ) + entities.append(TemperatureNumber(hass, unique_id, name, preset, True, True, entry.data)) + else: + for preset in CONF_PRESETS_AWAY_VALUES: + _LOGGER.debug( + "%s - configuring Number non central, non AC, AWAY for preset %s", + name, + preset, + ) + entities.append(TemperatureNumber(hass, unique_id, name, preset, False, True, entry.data)) + + # For central config only + else: + if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE): + entities.append(ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data)) + entities.append(ActivateBoilerPowerThresholdNumber(hass, unique_id, name, entry.data)) + + for preset in CONF_PRESETS_WITH_AC_VALUES: + _LOGGER.debug( + "%s - configuring Number central, AC, non AWAY for preset %s", + name, + preset, + ) + entities.append( + CentralConfigTemperatureNumber( + hass, unique_id, name, preset, True, False, entry.data + ) + ) + + for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES: + _LOGGER.debug( + "%s - configuring Number central, AC, AWAY for preset %s", name, preset + ) + entities.append( + CentralConfigTemperatureNumber( + hass, unique_id, name, preset, True, True, entry.data + ) + ) + + if len(entities) > 0: + async_add_entities(entities, True) + + +class ActivateBoilerThresholdNumber( + NumberEntity, RestoreEntity +): # pylint: disable=abstract-method + """Representation of the threshold of the number of VTherm + which should be active to activate the boiler""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the energy sensor""" + self._hass = hass + self._config_id = unique_id + self._device_name = entry_infos.get(CONF_NAME) + self._attr_name = "Number activation threshold" + self._attr_unique_id = "boiler_activation_threshold" + self._attr_value = self._attr_native_value = 0 # default value + self._attr_native_min_value = 0 + self._attr_native_max_value = 9 + self._attr_step = 1 # default value + self._attr_mode = NumberMode.AUTO + + @property + def icon(self) -> str | None: + if isinstance(self._attr_native_value, int): + val = int(self._attr_native_value) + return f"mdi:numeric-{val}-box-outline" + else: + return "mdi:numeric-0-box-outline" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + entry_type=None, + identifiers={(DOMAIN, self._config_id)}, + name=self._device_name, + manufacturer=DEVICE_MANUFACTURER, + model=DOMAIN, + ) + + @overrides + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass) + api.central_boiler_manager.register_central_boiler_activation_number_threshold(self) + + old_state: CoreState = await self.async_get_last_state() + _LOGGER.debug( + "%s - Calling async_added_to_hass old_state is %s", self, old_state + ) + if old_state is not None: + self._attr_value = self._attr_native_value = int(float(old_state.state)) + + @overrides + def set_native_value(self, value: float) -> None: + """Change the value""" + int_value = int(value) + old_value = int(self._attr_native_value) + + if int_value == old_value: + return + + self._attr_value = self._attr_native_value = int_value + self.hass.create_task(VersatileThermostatAPI.get_vtherm_api(self._hass).central_boiler_manager.refresh_central_boiler_custom_attributes()) + + def __str__(self): + return f"VersatileThermostat-{self.name}" + + +class ActivateBoilerPowerThresholdNumber(NumberEntity, RestoreEntity): # pylint: disable=abstract-method + """Representation of the threshold of the total power of VTherm + which should be active to activate the boiler""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the energy sensor""" + self._hass = hass + self._config_id = unique_id + self._device_name = entry_infos.get(CONF_NAME) + self._attr_name = "Power activation threshold" + self._attr_unique_id = "boiler_power_activation_threshold" + self._attr_value = self._attr_native_value = 0 # default value + self._attr_native_min_value = 0 + self._attr_native_max_value = 10000 # for people who works in Watts + self._attr_native_step = 0.1 + self._attr_mode = NumberMode.AUTO + + @property + def icon(self) -> str | None: + return "mdi:water-boiler-auto" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + entry_type=None, + identifiers={(DOMAIN, self._config_id)}, + name=self._device_name, + manufacturer=DEVICE_MANUFACTURER, + model=DOMAIN, + ) + + @overrides + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass) + api.central_boiler_manager.register_central_boiler_power_activation_threshold(self) + + old_state: CoreState = await self.async_get_last_state() + _LOGGER.debug("%s - Calling async_added_to_hass old_state is %s", self, old_state) + if old_state is not None: + self._attr_value = self._attr_native_value = float(old_state.state) + + @overrides + def set_native_value(self, value: float) -> None: + """Change the value""" + float_value = float(value) + old_value = float(self._attr_native_value) + + if float_value == old_value: + return + + self._attr_value = self._attr_native_value = float_value + self.hass.create_task(VersatileThermostatAPI.get_vtherm_api(self._hass).central_boiler_manager.refresh_central_boiler_custom_attributes()) + + def __str__(self): + return f"VersatileThermostat-{self.name}" + + +class CentralConfigTemperatureNumber( + NumberEntity, RestoreEntity +): # pylint: disable=abstract-method + """Representation of one temperature number""" + + _attr_has_entity_name = True + + def __init__( + self, + hass: HomeAssistant, + unique_id, + name, + preset_name, + is_ac, + is_away, + entry_infos, + ) -> None: + """Initialize the temperature with entry_infos if available. Else + the restoration will do the trick.""" + + self._config_id = unique_id + self._device_name = name + # self._attr_name = name + + self._attr_translation_key = preset_name + self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_preset_{preset_name}" + self._attr_unique_id = f"central_configuration_preset_{preset_name}" + self._attr_device_class = NumberDeviceClass.TEMPERATURE + self._attr_native_unit_of_measurement = hass.config.units.temperature_unit + + self._attr_native_step = entry_infos.get(CONF_STEP_TEMPERATURE, 0.5) + self._attr_native_min_value = entry_infos.get(CONF_TEMP_MIN) + self._attr_native_max_value = entry_infos.get(CONF_TEMP_MAX) + + # Initialize the values if included into the entry_infos. This will do + # the temperature migration. Else the temperature will be restored from + # previous value + # TODO remove this after the next major release and just keep the init min/max + temp = None + if (temp := entry_infos.get(preset_name, None)) is not None: + self._attr_value = self._attr_native_value = temp + else: + if entry_infos.get(CONF_AC_MODE) is True: + self._attr_native_value = self._attr_native_max_value + else: + self._attr_native_value = self._attr_native_min_value + + self._attr_mode = NumberMode.BOX + self._preset_name = preset_name + self._is_away = is_away + self._is_ac = is_ac + + @property + def icon(self) -> str | None: + return PRESET_ICON_MAPPING[self._preset_name] + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + entry_type=None, + identifiers={(DOMAIN, self._config_id)}, + name=self._device_name, + manufacturer=DEVICE_MANUFACTURER, + model=DOMAIN, + ) + + @overrides + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + + # register the temp entity for this device and preset + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass) + api.register_temperature_number(self._config_id, self._preset_name, self) + + # Restore value from previous one if exists + old_state: CoreState = await self.async_get_last_state() + _LOGGER.debug( + "%s - Calling async_added_to_hass old_state is %s", self, old_state + ) + try: + if old_state is not None and ((value := float(old_state.state)) > 0): + self._attr_value = self._attr_native_value = value + except ValueError: + pass + + @overrides + async def async_set_native_value(self, value: float) -> None: + """The value have change from the Number Entity in UI""" + float_value = float(value) + old_value = ( + None if self._attr_native_value is None else float(self._attr_native_value) + ) + + if float_value == old_value: + return + + self._attr_value = self._attr_native_value = float_value + + # persist the value + self.async_write_ha_state() + + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass) + api.register_temperature_number(self._config_id, self._preset_name, self) + + # We have to reload all VTherm for which uses the central configuration + # Update the VTherms which have temperature in central config + self.hass.create_task(api.init_vtherm_preset_with_central()) + + def __str__(self): + return f"VersatileThermostat-{self.name}" + + @property + def native_unit_of_measurement(self) -> str | None: + """The unit of measurement""" + return self.hass.config.units.temperature_unit + + +class TemperatureNumber( # pylint: disable=abstract-method + VersatileThermostatBaseEntity, NumberEntity, RestoreEntity +): + """Representation of one temperature number""" + + _attr_has_entity_name = True + + def __init__( + self, + hass: HomeAssistant, + unique_id, + name, + preset_name, + is_ac, + is_away, + entry_infos, + ) -> None: + """Initialize the temperature with entry_infos if available. Else + the restoration will do the trick.""" + super().__init__(hass, unique_id, name) + + self._attr_translation_key = preset_name + self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_preset_{preset_name}" + + self._attr_unique_id = f"{self._device_name}_preset_{preset_name}" + self._attr_device_class = NumberDeviceClass.TEMPERATURE + self._attr_entity_category = EntityCategory.CONFIG + self._attr_native_unit_of_measurement = hass.config.units.temperature_unit + + self._has_central_main_attributes = entry_infos.get( + CONF_USE_MAIN_CENTRAL_CONFIG, False + ) + + self.init_min_max_step(entry_infos) + + # Initialize the values if included into the entry_infos. This will do + # the temperature migration. + temp = None + if (temp := entry_infos.get(preset_name, None)) is not None: + self._attr_value = self._attr_native_value = temp + else: + if entry_infos.get(CONF_AC_MODE) is True: + self._attr_native_value = self._attr_native_max_value + else: + self._attr_native_value = self._attr_native_min_value + + self._attr_mode = NumberMode.BOX + self._preset_name = preset_name + self._canonical_preset_name = preset_name.replace( + PRESET_TEMP_SUFFIX, "" + ).replace(PRESET_AWAY_SUFFIX, "") + self._is_away = is_away + self._is_ac = is_ac + + @property + def icon(self) -> str | None: + return PRESET_ICON_MAPPING[self._preset_name] + + @overrides + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + + # register the temp entity for this device and preset + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass) + api.register_temperature_number(self._config_id, self._preset_name, self) + + old_state: CoreState = await self.async_get_last_state() + _LOGGER.debug( + "%s - Calling async_added_to_hass old_state is %s", self, old_state + ) + try: + if old_state is not None and ((value := float(old_state.state)) > 0): + self._attr_value = self._attr_native_value = value + except ValueError: + pass + + @overrides + def my_climate_is_initialized(self): + """Called when the associated climate is initialized""" + self._attr_native_step = self.my_climate.target_temperature_step + self._attr_native_min_value = self.my_climate.min_temp + self._attr_native_max_value = self.my_climate.max_temp + return + + @overrides + async def async_set_native_value(self, value: float) -> None: + """Change the value""" + + if self.my_climate is None: + _LOGGER.warning( + "%s - cannot change temperature because VTherm is not initialized", self + ) + return + + float_value = float(value) + old_value = ( + None if self._attr_native_value is None else float(self._attr_native_value) + ) + + if float_value == old_value: + return + + self._attr_value = self._attr_native_value = float_value + self.async_write_ha_state() + + # Update the VTherm temp + self.hass.create_task( + self.my_climate.set_preset_temperature( + self._canonical_preset_name, + self._attr_native_value if not self._is_away else None, + self._attr_native_value if self._is_away else None, + ) + ) + + # We set the min, max and step from central config if relevant because it is possible + # that central config was not loaded at startup + self.init_min_max_step() + + def __str__(self): + return f"VersatileThermostat-{self.name}" + + @property + def native_unit_of_measurement(self) -> str | None: + """The unit of measurement""" + if not self.my_climate: + return self.hass.config.units.temperature_unit + return self.my_climate.temperature_unit + + def init_min_max_step(self, entry_infos=None): + """Initialize min, max and step value from config or from central config""" + if self._has_central_main_attributes: + vthermapi: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api() + central_config = vthermapi.find_central_configuration() + if central_config: + self._attr_native_step = central_config.data.get(CONF_STEP_TEMPERATURE) + self._attr_native_min_value = central_config.data.get(CONF_TEMP_MIN) + self._attr_native_max_value = central_config.data.get(CONF_TEMP_MAX) + + return + + if entry_infos: + self._attr_native_step = entry_infos.get( + CONF_STEP_TEMPERATURE, DEFAULT_STEP + ) + self._attr_native_min_value = entry_infos.get( + CONF_TEMP_MIN, DEFAULT_MIN_VALUE + ) + self._attr_native_max_value = entry_infos.get( + CONF_TEMP_MAX, DEFAULT_MAX_VALUE + ) diff --git a/custom_components/versatile_thermostat/open_window_algorithm.py b/custom_components/versatile_thermostat/open_window_algorithm.py new file mode 100644 index 0000000..592f8b5 --- /dev/null +++ b/custom_components/versatile_thermostat/open_window_algorithm.py @@ -0,0 +1,154 @@ +# pylint: disable=line-too-long +""" 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 vtherm_api.log_collector import get_vtherm_logger +from datetime import datetime + +_LOGGER = get_vtherm_logger(__name__) + +# To filter bad values +MIN_DELTA_T_SEC = 0 # two temp mesure should be > 0 sec +MAX_SLOPE_VALUE = ( + 120 # slope cannot be > 2°/min or < -2°/min -> else this is an aberrant point +) + +MAX_DURATION_MIN = 30 # a fake data point is added in the cycle if last measurement was older than 30 min + +MIN_NB_POINT = 4 # do not calculate slope until we have enough point + + +class WindowOpenDetectionAlgorithm: + """The class that implements the algorithm listed above""" + + def __init__(self, alert_threshold, end_alert_threshold, vtherm=None) -> None: + """Initalize a new algorithm with the both threshold""" + 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 + self._vtherm = vtherm + + def check_age_last_measurement(self, temperature, datetime_now) -> float: + """ " Check if last measurement is old and add + a fake measurement point if this is the case + """ + if self._last_datetime is None: + return self.add_temp_measurement(temperature, datetime_now) + + delta_t_sec = float((datetime_now - self._last_datetime).total_seconds()) / 60.0 + if delta_t_sec >= MAX_DURATION_MIN: + return self.add_temp_measurement(temperature, datetime_now, False) + else: + # do nothing + return self._last_slope + + def add_temp_measurement( + self, temperature: float, datetime_measure: datetime, store_date: bool = True + ) -> float: + """Add a new temperature measurement + returns the last slope + """ + if self._last_datetime is None or self._last_temperature is None: + _LOGGER.debug("%s - First initialisation", self) + self._last_datetime = datetime_measure + self._last_temperature = temperature + self._nb_point = self._nb_point + 1 + return None + + _LOGGER.debug( + "%s - We are already initialized slope=%s last_temp=%0.2f", + self, + self._last_slope, + self._last_temperature, + ) + lspe = self._last_slope + + delta_t_sec = float((datetime_measure - self._last_datetime).total_seconds()) + delta_t = delta_t_sec / 60.0 + if delta_t_sec <= MIN_DELTA_T_SEC: + _LOGGER.debug( + "%s - Delta t is %d < %d which should be not possible. We don't consider this value", + self, + delta_t_sec, + MIN_DELTA_T_SEC, + ) + return lspe + + delta_t_hour = delta_t / 60.0 + + delta_temp = float(temperature - self._last_temperature) + new_slope = delta_temp / delta_t_hour + if new_slope > MAX_SLOPE_VALUE or new_slope < -MAX_SLOPE_VALUE: + _LOGGER.debug( + "%s - New_slope is abs(%.2f) > %.2f which should be not possible. We don't consider this value", + self, + new_slope, + MAX_SLOPE_VALUE, + ) + return lspe + + if self._last_slope is None: + self._last_slope = round(new_slope, 2) + else: + self._last_slope = round((0.2 * self._last_slope) + (0.8 * new_slope), 2) + + # if we are in cycle check and so adding a fake datapoint, we don't store the event datetime + # so that, when we will receive a real temperature point we will not calculate a wrong slope + if store_date: + self._last_datetime = datetime_measure + + self._last_temperature = temperature + + self._nb_point = self._nb_point + 1 + _LOGGER.debug( + "%s - delta_t=%.3f delta_temp=%.3f new_slope=%.3f last_slope=%s slope=%.3f nb_point=%s", + self, + delta_t, + delta_temp, + new_slope, + lspe, + self._last_slope, + self._nb_point, + ) + + 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 + + if self._nb_point < MIN_NB_POINT or self._last_slope is None: + return False + + return self._last_slope < -self._alert_threshold + + 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 + + if self._nb_point < MIN_NB_POINT or self._last_slope is None: + return False + + return self._last_slope >= self._end_alert_threshold + + @property + def last_slope(self) -> float: + """Return the last calculated slope""" + return self._last_slope + + def __str__(self) -> str: + if self._vtherm and hasattr(self._vtherm, "name"): + return f"{self._vtherm.name}-WindowOpenDetectionAlgorithm" + else: + return f"UnknownVTherm-WindowOpenDetectionAlgorithm" diff --git a/custom_components/versatile_thermostat/opening_degree_algorithm.py b/custom_components/versatile_thermostat/opening_degree_algorithm.py new file mode 100644 index 0000000..00fd57f --- /dev/null +++ b/custom_components/versatile_thermostat/opening_degree_algorithm.py @@ -0,0 +1,71 @@ +""" This class aims to calculate the opening/closing degree of a valve. +See: https://github.com/jmcollin78/versatile_thermostat/issues/1220 """ + +import logging +from vtherm_api.log_collector import get_vtherm_logger + +_LOGGER = get_vtherm_logger(__name__) + + +class OpeningClosingDegreeCalculation: + """Class to calculate the opening/closing degree of a valve.""" + + @staticmethod + def calculate_opening_closing_degree( + brut_valve_open_percent: float, + min_opening_degree: float, + max_closing_degree: float, + max_opening_degree: float, + opening_threshold: float + ) -> float: + """ + Calculate the opening/closing degree based on parameters. See explanation on README + + Args: + brut_valve_open_percent: Raw valve opening percentage (0-100) + min_opening_degree: Minimum opening degree + max_closing_degree: Maximum closing degree + max_opening_degree: Maximum opening degree + opening_threshold: Opening threshold + + Returns: + float: The calculated opening degree + float: The calculated closing degree + """ + _LOGGER.debug( + "Calculate opening/closing degree - Input: brut_valve_open_percent=%.2f, min_opening_degree=%.2f, max_closing_degree=%.2f", + brut_valve_open_percent, + min_opening_degree, + max_closing_degree + ) + + # for direct test. Already done in underlyings.py + if min_opening_degree >= max_opening_degree: + min_opening_degree = opening_threshold + + # clamp the brut_valve_open_percent to be within 0 and 100 + brut_valve_open_percent = max(0, min(100, brut_valve_open_percent)) + + # normalize to 0-1 range + bvop = brut_valve_open_percent / 100.0 + min_od = min_opening_degree / 100.0 + max_cd = max_closing_degree / 100.0 + max_od = max_opening_degree / 100.0 + ot = opening_threshold / 100.0 + + # if heating need is >= opening_threshold (and heating is > 0) -> open and calculate with interpolation, + if bvop >= ot and bvop > 0: + # interpolation is just here to normalize the max opening which can be != 100. Some TRV has a max which not 100 + slope = (max_od - min_od) / (1 - ot) + calculated_degree = min_od + slope * (bvop - ot) + else: + calculated_degree = 1 - max_cd + + # set to base 100 + calculated_degree = round(calculated_degree * 100) + _LOGGER.debug( + "Calculate opening/closing degree - Output: calculated_degree=%.2f %%", + calculated_degree, + ) + + return calculated_degree, 100 - calculated_degree diff --git a/custom_components/versatile_thermostat/pi_algorithm.py b/custom_components/versatile_thermostat/pi_algorithm.py new file mode 100644 index 0000000..6fabb41 --- /dev/null +++ b/custom_components/versatile_thermostat/pi_algorithm.py @@ -0,0 +1,124 @@ +# pylint: disable=line-too-long +""" The PI algorithm implementation """ + +import logging +from vtherm_api.log_collector import get_vtherm_logger + +_LOGGER = get_vtherm_logger(__name__) + + +class PITemperatureRegulator: + """A class implementing a PI Algorithm + PI algorithms calculate a target temperature by adding an offset which is calculating as follow: + - offset = kp * error + ki * accumulated_error + + To use it you must: + - instanciate the class and gives the algorithm parameters: kp, ki, offset_max, accumulated_error_threshold, overheat_protection + - call calculate_regulated_temperature with the internal, external temperature and time_delta. Time_delta is 1.0 for a standard regulation cycle. + - call set_target_temp when the target temperature change. + """ + + def __init__( + self, + target_temp: float, + kp: float, + ki: float, + k_ext: float, + offset_max: float, + accumulated_error_threshold: float, + overheat_protection: bool, + ): + self.target_temp: float = target_temp + self.kp: float = kp # proportionnel gain + self.ki: float = ki # integral gain + self.k_ext: float = k_ext # exterior gain + self.offset_max: float = offset_max + self.accumulated_error: float = 0 + self.accumulated_error_threshold: float = accumulated_error_threshold + self.overheat_protection: bool = overheat_protection + + def reset_accumulated_error(self): + """Reset the accumulated error""" + self.accumulated_error = 0 + + def set_accumulated_error(self, accumulated_error): + """Allow to persist and restore the accumulated_error""" + self.accumulated_error = accumulated_error + + def set_target_temp(self, target_temp): + """Set the new target_temp""" + self.target_temp = target_temp + # Discussion #191. After a target change we should reset the accumulated error which is certainly wrong now. + # Discussion #384. Finally don't reset the accumulated error but smoothly reset it if the sign is inversed + # if self.accumulated_error < 0: + # self.accumulated_error = 0 + + def calculate_regulated_temperature(self, room_temp: float | None, external_temp: float | None, time_delta: float): # pylint: disable=unused-argument + """Calculate a new target_temp given some temperature""" + if room_temp is None: + _LOGGER.warning( + "Temporarily skipping the self-regulation algorithm while the configured sensor for room temperature is unavailable" + ) + return self.target_temp + if external_temp is None: + _LOGGER.warning( + "Temporarily skipping the self-regulation algorithm while the configured sensor for outdoor temperature is unavailable" + ) + return self.target_temp + if time_delta > 2.0: + # When the HVAC mode is off (by on/off or manual change), the PI algorithm is not run. time_delta can be + # very high and broke the algo. + + # It should never be higher than 1 on normal run but sometime the asyncio have a small latency on system + # with low performance. Allowing a value a little over 1 give a better response. + + # Until 2.0, the resulted offset should be good. Upper it can unbalance the regulation so it is caped to + # 1.0. + _LOGGER.info( + "The time delta (%.2f) is too high for the self-regulation algorithm. Capping to 1.0.", + time_delta, + ) + time_delta = 1.0 + + # Calculate the error factor (P) + error = self.target_temp - room_temp + + # Calculate the sum of error (I) + # Discussion #384. Finally don't reset the accumulated error but smoothly reset it if the sign is inversed + # If the error have change its sign, reset smoothly the accumulated error + if self.overheat_protection and error * self.accumulated_error < 0: + self.accumulated_error = self.accumulated_error / (2.0 * time_delta) + + self.accumulated_error += error * time_delta + + # Capping of the error + self.accumulated_error = min( + self.accumulated_error_threshold, + max(-self.accumulated_error_threshold, self.accumulated_error), + ) + + # Calculate the offset (proportionnel + intégral) + offset = self.kp * error + self.ki * self.accumulated_error + + # Calculate the exterior offset + offset_ext = self.k_ext * (room_temp - external_temp) + + # Capping of offset + total_offset = offset + offset_ext + total_offset = min(self.offset_max, max(-self.offset_max, total_offset)) + + result = round(self.target_temp + total_offset, 1) + + _LOGGER.debug( + "PITemperatureRegulator - Error: %.2f accumulated_error: %.2f (overheat protection %s and delta %.2f) offset: %.2f offset_ext: %.2f target_tem: %.1f regulatedTemp: %.1f", + error, + self.accumulated_error, + self.overheat_protection, + time_delta, + offset, + offset_ext, + self.target_temp, + result, + ) + + return result diff --git a/custom_components/versatile_thermostat/prop_algo_tpi.py b/custom_components/versatile_thermostat/prop_algo_tpi.py new file mode 100644 index 0000000..eafc0ce --- /dev/null +++ b/custom_components/versatile_thermostat/prop_algo_tpi.py @@ -0,0 +1,227 @@ +""" The TPI calculation module """ +# pylint: disable='line-too-long' +import logging +from vtherm_api.log_collector import get_vtherm_logger + +from .vtherm_hvac_mode import VThermHvacMode, VThermHvacMode_OFF, VThermHvacMode_COOL, VThermHvacMode_SLEEP + +_LOGGER = get_vtherm_logger(__name__) + + +def is_number(value): + """check if value is a number""" + return isinstance(value, (int, float)) + + +class TpiAlgorithm: + """This class aims to do all calculation of the Proportional alogorithm""" + + def __init__( + self, + + tpi_coef_int, + tpi_coef_ext, + vtherm_entity_id: str = None, + max_on_percent: float = None, + tpi_threshold_low: float = 0.0, + tpi_threshold_high: float = 0.0, + ) -> None: + """Initialisation of the Proportional Algorithm""" + _LOGGER.debug( + "%s - Creation new TpiAlgorithm tpi_coef_int: %s, tpi_coef_ext: %s, tpi_threshold_low=%s, tpi_threshold_high=%s", # pylint: disable=line-too-long + vtherm_entity_id, + + tpi_coef_int, + tpi_coef_ext, + tpi_threshold_low, + tpi_threshold_high, + ) + + # Issue 506 - check parameters + if ( + vtherm_entity_id is None + or not is_number(tpi_coef_int) + or not is_number(tpi_coef_ext) + ): + _LOGGER.error( + "%s - configuration is wrong. entity_id is %s, tpi_coef_int is %s, tpi_coef_ext is %s", + vtherm_entity_id, + vtherm_entity_id, + tpi_coef_int, + tpi_coef_ext, + ) + raise TypeError( + "TPI parameters are not set correctly. VTherm will not work as expected. Please reconfigure it correctly. See previous log for values" + ) + + self._vtherm_entity_id = vtherm_entity_id + + self._tpi_coef_int = tpi_coef_int + self._tpi_coef_ext = tpi_coef_ext + self._on_percent = 0 + self._calculated_on_percent = 0 + self._total_on_percent = 0 + self._max_on_percent = max_on_percent + self._tpi_threshold_low = tpi_threshold_low + self._tpi_threshold_high = tpi_threshold_high + self._apply_threshold = tpi_threshold_low != 0.0 and tpi_threshold_high != 0.0 + # True once calculate() has been called with a valid temperature. + # When False, on_percent returns None so the cycle scheduler + # does not touch an already-active switch at startup. + self._temperature_available: bool = False + + def calculate( + self, + target_temp: float | None, + current_temp: float | None, + ext_current_temp: float | None, + slope: float | None, + hvac_mode: VThermHvacMode, + **kwargs, + ): + """Do the calculation of the duration""" + if target_temp is None or current_temp is None: + log = _LOGGER.debug if hvac_mode == VThermHvacMode_OFF else _LOGGER.warning + log( + "%s - Proportional algorithm: calculation is not possible cause target_temp (%s) or current_temp (%s) is null. Heating/cooling will be disabled. This could be normal at startup", # pylint: disable=line-too-long + self._vtherm_entity_id, + target_temp, + current_temp, + ) + self._calculated_on_percent = 0 + self._temperature_available = False + else: + self._temperature_available = True + if hvac_mode == VThermHvacMode_COOL: + delta_temp = current_temp - target_temp + delta_ext_temp = ext_current_temp - target_temp if ext_current_temp is not None else 0 + slope = -slope if slope is not None else None + else: + delta_temp = target_temp - current_temp + delta_ext_temp = target_temp - ext_current_temp if ext_current_temp is not None else 0 + + # Apply thresholds + if ( + # fmt: off + self._apply_threshold + and slope is not None + and ((slope > 0.0 and -delta_temp > self._tpi_threshold_high) + or (slope < 0.0 and -delta_temp > self._tpi_threshold_low)) + # fmt: on + ): + _LOGGER.debug( + "%s - Proportional algorithm: on_percent is forced to 0 cause current_temp (%.1f) is outside the thresholds (slope=%.1f, target_temp=%.1f, tpi_threshold_low=%.1f, tpi_threshold_high=%.1f). Heating/cooling will be disabled.", # pylint: disable=line-too-long + self._vtherm_entity_id, + current_temp, + slope, + target_temp, + self._tpi_threshold_low, + self._tpi_threshold_high, + ) + self._calculated_on_percent = 0 + else: + if hvac_mode not in [VThermHvacMode_OFF, VThermHvacMode_SLEEP]: + self._calculated_on_percent = self._tpi_coef_int * delta_temp + self._tpi_coef_ext * delta_ext_temp + # calculated on_time duration in seconds + if self._calculated_on_percent > 1: + self._calculated_on_percent = 1 + if self._calculated_on_percent < 0: + self._calculated_on_percent = 0 + + if self._max_on_percent is not None and self._calculated_on_percent > self._max_on_percent: + self._calculated_on_percent = self._max_on_percent + else: + _LOGGER.debug( + "%s - Proportional algorithm: VTherm is off. Heating will be disabled", + self._vtherm_entity_id, + ) + self._calculated_on_percent = 0 + + _LOGGER.debug( + "%s - heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f", # pylint: disable=line-too-long + self._vtherm_entity_id, + current_temp if current_temp else -9999.0, + ext_current_temp if ext_current_temp else -9999.0, + target_temp if target_temp else -9999.0, + self._calculated_on_percent, + ) + + def update_parameters( + self, + tpi_coef_int=None, + tpi_coef_ext=None, + tpi_threshold_low=None, + tpi_threshold_high=None, + ): + """Update the parameters of the algorithm""" + if tpi_coef_int is not None: + self._tpi_coef_int = tpi_coef_int + if tpi_coef_ext is not None: + self._tpi_coef_ext = tpi_coef_ext + if tpi_threshold_low is not None: + self._tpi_threshold_low = tpi_threshold_low + if tpi_threshold_high is not None: + self._tpi_threshold_high = tpi_threshold_high + + self._apply_threshold = self._tpi_threshold_low != 0.0 and self._tpi_threshold_high != 0.0 + _LOGGER.debug( + "%s - Parameters updated. tpi_coef_int: %s, tpi_coef_ext: %s, tpi_threshold_low: %s, tpi_threshold_high: %s", + self._vtherm_entity_id, + self._tpi_coef_int, + self._tpi_coef_ext, + self._tpi_threshold_low, + self._tpi_threshold_high, + ) + + def update_realized_power(self, power_percent: float): + """Update the realized power_percent. + This method is called by the VTherm when the power is modified by safety or other features + which clamps the value or force it to 0 or 1. + """ + self._total_on_percent = power_percent + if self._total_on_percent != self._calculated_on_percent: + _LOGGER.debug( + "%s - Realized power is different from calculated power. Calculated: %.2f, Realized: %.2f", + self._vtherm_entity_id, + self._calculated_on_percent, + self._total_on_percent, + ) + + @property + def on_percent(self) -> float | None: + """Returns the percentage the heater must be ON. + Returns None when no valid temperature was available at the last calculate() call. + In that case callers should preserve the current switch state rather than forcing it off. + Note: This does NOT reflect safety overrides. + Use calculated_on_percent for display or check ThermostatProp.on_percent for the effective value. + """ + if not self._temperature_available: + return None + return round(self._calculated_on_percent, 2) + + @property + def calculated_on_percent(self) -> float: + """Returns the calculated percentage the heater must be ON + Calculated means NOT overriden even in safety mode + (1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long + return round(self._calculated_on_percent, 2) + + @property + def tpi_coef_int(self) -> float: + """Returns the TPI coefficient int""" + return self._tpi_coef_int + + @property + def tpi_coef_ext(self) -> float: + """Returns the TPI coefficient ext""" + return self._tpi_coef_ext + + @property + def tpi_threshold_low(self) -> float: + """Returns the TPI threshold low""" + return self._tpi_threshold_low + + @property + def tpi_threshold_high(self) -> float: + """Returns the TPI threshold high""" + return self._tpi_threshold_high diff --git a/custom_components/versatile_thermostat/prop_handler_tpi.py b/custom_components/versatile_thermostat/prop_handler_tpi.py new file mode 100644 index 0000000..e8b598e --- /dev/null +++ b/custom_components/versatile_thermostat/prop_handler_tpi.py @@ -0,0 +1,638 @@ +# pylint: disable=line-too-long, abstract-method +"""TPI algorithm handler for ThermostatProp.""" + +import logging +from vtherm_api.log_collector import get_vtherm_logger +from typing import Any, TYPE_CHECKING +from datetime import datetime + +from homeassistant.exceptions import ServiceValidationError + +from .prop_algo_tpi import TpiAlgorithm +from .auto_tpi_manager import AutoTpiManager +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .vtherm_hvac_mode import VThermHvacMode_OFF, VThermHvacMode_HEAT, VThermHvacMode_COOL +from .vtherm_central_api import VersatileThermostatAPI +from .commons import write_event_log +from .cycle_scheduler import calculate_cycle_times + +if TYPE_CHECKING: + from .thermostat_prop import ThermostatProp + +_LOGGER = get_vtherm_logger(__name__) + + +class TPIHandler: + """Handler for TPI-specific logic. + + This class encapsulates all TPI-specific behavior and is used + via composition by ThermostatProp. It updates thermostat attributes + directly for backward compatibility with child classes. + """ + + def __init__(self, thermostat: "ThermostatProp"): + """Initialize handler with parent thermostat reference.""" + self._thermostat = thermostat + self._auto_tpi_manager: AutoTpiManager | None = None + # Save default values for Auto TPI reset + self._default_coef_int: float = 0 + self._default_coef_ext: float = 0 + + @property + def tpi_coef_int(self) -> float: + """Return TPI internal coefficient from thermostat.""" + return self._thermostat.tpi_coef_int + + @property + def tpi_coef_ext(self) -> float: + """Return TPI external coefficient from thermostat.""" + return self._thermostat.tpi_coef_ext + + @property + def tpi_threshold_low(self) -> float: + """Return TPI low threshold from thermostat.""" + return self._thermostat.tpi_threshold_low + + @property + def tpi_threshold_high(self) -> float: + """Return TPI high threshold from thermostat.""" + return self._thermostat.tpi_threshold_high + + @property + def minimal_activation_delay(self) -> int: + """Return minimal activation delay from thermostat.""" + return self._thermostat.minimal_activation_delay + + @property + def minimal_deactivation_delay(self) -> int: + """Return minimal deactivation delay from thermostat.""" + return self._thermostat.minimal_deactivation_delay + + @property + def proportional_function(self) -> str | None: + """Return proportional function from thermostat.""" + return self._thermostat.proportional_function + + @property + def auto_tpi_manager(self) -> AutoTpiManager | None: + """Return the Auto TPI manager.""" + return self._auto_tpi_manager + + def init_algorithm(self): + """Initialize PropAlgorithm and AutoTpiManager. + + Updates thermostat attributes directly for backward compatibility. + """ + t = self._thermostat + entry = t.entry_infos + + # Read and set TPI-specific config on thermostat using public setters + t.tpi_coef_int = entry.get(CONF_TPI_COEF_INT) + t.tpi_coef_ext = entry.get(CONF_TPI_COEF_EXT) + t.tpi_threshold_low = entry.get(CONF_TPI_THRESHOLD_LOW, 0.0) + t.tpi_threshold_high = entry.get(CONF_TPI_THRESHOLD_HIGH, 0.0) + t.minimal_activation_delay = entry.get(CONF_MINIMAL_ACTIVATION_DELAY, 0) + t.minimal_deactivation_delay = entry.get(CONF_MINIMAL_DEACTIVATION_DELAY, 0) + + # Save default values for Auto TPI reset + self._default_coef_int = t.tpi_coef_int + self._default_coef_ext = t.tpi_coef_ext + + # Validation: thresholds - if one is 0 then both are 0 + if t.tpi_threshold_low == 0.0 or t.tpi_threshold_high == 0.0: + t.tpi_threshold_low = 0.0 + t.tpi_threshold_high = 0.0 + + # Validation: delays + if (t.minimal_activation_delay + t.minimal_deactivation_delay) / 60 > t.cycle_min: + _LOGGER.warning( + "%s - The sum of minimal_activation_delay (%s sec) and " + "minimal_deactivation_delay (%s sec) is greater than cycle_min (%s). " + "This can create some unexpected behavior. Please review your configuration", + t, + t.minimal_activation_delay, + t.minimal_deactivation_delay, + t.cycle_min, + ) + + # Validation: external temp sensor for TPI + if ( + t.proportional_function == PROPORTIONAL_FUNCTION_TPI + and t.ext_temp_sensor_entity_id is None + ): + _LOGGER.warning( + "Using TPI function but not external temperature sensor is set. " + "Removing the delta temp ext factor. " + "Thermostat will not be fully operational." + ) + t.tpi_coef_ext = 0 + + # Create TpiAlgorithm on thermostat + t.prop_algorithm = TpiAlgorithm( + t.tpi_coef_int, + t.tpi_coef_ext, + t.name, + max_on_percent=t.max_on_percent, + tpi_threshold_low=t.tpi_threshold_low, + tpi_threshold_high=t.tpi_threshold_high, + ) + + # Initialize Auto TPI Manager from config + heater_heating_time = entry.get(CONF_AUTO_TPI_HEATER_HEATING_TIME, 5) + heater_cooling_time = entry.get(CONF_AUTO_TPI_HEATER_COOLING_TIME, 5) + calculation_method = entry.get(CONF_AUTO_TPI_CALCULATION_METHOD, AUTO_TPI_METHOD_EMA) + ema_alpha = entry.get(CONF_AUTO_TPI_EMA_ALPHA, 0.2) + avg_initial_weight = entry.get(CONF_AUTO_TPI_AVG_INITIAL_WEIGHT, 1) + + heating_rate = entry.get(CONF_AUTO_TPI_HEATING_POWER, 1.0) + cooling_rate = entry.get(CONF_AUTO_TPI_COOLING_POWER, 1.0) + aggressiveness = entry.get(CONF_AUTO_TPI_AGGRESSIVENESS, 1.0) + continuous_kext = entry.get(CONF_AUTO_TPI_CONTINUOUS_KEXT, False) + continuous_kext_alpha = entry.get(CONF_AUTO_TPI_CONTINUOUS_KEXT_ALPHA, 0.04) + + _LOGGER.info("%s - DEBUG: TPI coefficients from entry_infos: int=%.3f, ext=%.3f", + t, t.tpi_coef_int, t.tpi_coef_ext) + + self._auto_tpi_manager = AutoTpiManager( + t.hass, + t.config_entry, + t.unique_id, + t.name, + t.cycle_min, + t.tpi_threshold_low, + t.tpi_threshold_high, + t.minimal_deactivation_delay, + coef_int=t.tpi_coef_int, + coef_ext=t.tpi_coef_ext, + heater_heating_time=heater_heating_time, + heater_cooling_time=heater_cooling_time, + calculation_method=calculation_method, + ema_alpha=ema_alpha, + avg_initial_weight=avg_initial_weight, + heating_rate=heating_rate, + cooling_rate=cooling_rate, + aggressiveness=aggressiveness, + continuous_kext=continuous_kext, + continuous_kext_alpha=continuous_kext_alpha, + ) + self._auto_tpi_manager.set_is_vtherm_stopping_callback(lambda: t.is_removed) + _LOGGER.info("%s - DEBUG: AutoTpiManager initialized with defaults: int=%.3f, ext=%.3f", + t, self._auto_tpi_manager._default_coef_int, self._auto_tpi_manager._default_coef_ext) + + async def async_added_to_hass(self): + """Load Auto TPI data.""" + t = self._thermostat + if self._auto_tpi_manager: + # Set entity_id for pre-bootstrap calibration sensor lookup + self._auto_tpi_manager._entity_id = t.entity_id + _LOGGER.info("%s - DEBUG: Before load_data - int=%.3f, ext=%.3f", t, t.tpi_coef_int, t.tpi_coef_ext) + await self._auto_tpi_manager.async_load_data() + + # If we have learned parameters, apply them + learned_params = self._auto_tpi_manager.get_calculated_params() + if learned_params: + _LOGGER.info("%s - DEBUG: Learned params found: %s, learning_active=%s", + t, learned_params, self._auto_tpi_manager.learning_active) + if self._auto_tpi_manager.learning_active: + t.tpi_coef_int = learned_params.get(CONF_TPI_COEF_INT, t.tpi_coef_int) + t.tpi_coef_ext = learned_params.get(CONF_TPI_COEF_EXT, t.tpi_coef_ext) + _LOGGER.info("%s - Restored Auto TPI parameters: %s", t, learned_params) + else: + _LOGGER.info("%s - Auto TPI parameters found but not applied because learning is disabled", t) + + _LOGGER.info("%s - DEBUG: After load_data - int=%.3f, ext=%.3f", + t, t.tpi_coef_int, t.tpi_coef_ext) + + if self._auto_tpi_manager.learning_active: + # Security: if the feature is disabled in config, we must stop learning + if not t.entry_infos.get(CONF_AUTO_TPI_MODE, False): + _LOGGER.info("%s - Auto TPI learning was active but feature is disabled in config. Stopping learning.", t) + await self._auto_tpi_manager.stop_learning() + else: + _LOGGER.info("%s - Auto TPI learning is active (restored from storage)", t) + + async def async_startup(self): + """Startup actions.""" + # No more active cycle loop to start, it's now handled passively in control_heating + pass + + def remove(self): + """Cleanup on removal.""" + t = self._thermostat + if self._auto_tpi_manager: + t.hass.async_create_task(self._auto_tpi_manager.async_save_data()) + + def on_scheduler_ready(self, scheduler) -> None: + """Register AutoTPI learning callbacks on the cycle scheduler.""" + if self._auto_tpi_manager: + scheduler.register_cycle_start_callback(self._auto_tpi_manager.on_cycle_started) + scheduler.register_cycle_end_callback(self._auto_tpi_manager.on_cycle_completed) + + def should_publish_intermediate(self) -> bool: + """TPI always publishes the current control iteration.""" + return True + + def _is_central_boiler_off(self) -> bool: + """Check if the central boiler is configured but currently off.""" + t = self._thermostat + if not t.is_used_by_central_boiler: + return False + api = VersatileThermostatAPI.get_vtherm_api() + if api and api.central_boiler_manager: + return not api.central_boiler_manager.is_on + return False + + async def _get_tpi_data(self) -> dict[str, Any]: + """Calculate and return TPI cycle parameters.""" + t = self._thermostat + + # Feed current temperatures to AutoTpiManager BEFORE getting params + if self._auto_tpi_manager: + await self._auto_tpi_manager.update( + room_temp=t.current_temperature, + ext_temp=t.current_outdoor_temperature, + target_temp=t.target_temperature, + hvac_mode=str(t.vtherm_hvac_mode), + is_overpowering_detected=t.power_manager.is_overpowering_detected, + is_central_boiler_off=self._is_central_boiler_off(), + is_heating_failure=t.heating_failure_detection_manager.is_failure_detected, + ) + + # Sync coefficients from AutoTpiManager before calculating + if self._auto_tpi_manager and self._auto_tpi_manager.learning_active: + new_params = await self._auto_tpi_manager.calculate() + if new_params: + new_coef_int = new_params.get(CONF_TPI_COEF_INT) + new_coef_ext = new_params.get(CONF_TPI_COEF_EXT) + if new_coef_int != t.prop_algorithm.tpi_coef_int or \ + new_coef_ext != t.prop_algorithm.tpi_coef_ext: + t.prop_algorithm.update_parameters(tpi_coef_int=new_coef_int, tpi_coef_ext=new_coef_ext) + _LOGGER.debug("%s - Synced TPI coeffs before cycle: int=%.3f, ext=%.3f", + t, new_coef_int, new_coef_ext) + + # Force recalculation with potentially updated coefficients + t.recalculate() + + # Calculate time (in seconds) + if t.prop_algorithm: + on_time_sec, off_time_sec, forced_by_timing = calculate_cycle_times( + t.on_percent, + t.cycle_min, + t.minimal_activation_delay, + t.minimal_deactivation_delay, + ) + + realized_percent = on_time_sec / (t.cycle_min * 60) + + # Notify prop_algorithm if forced by timing (existing behavior) + if forced_by_timing: + if t.prop_algorithm and hasattr(t.prop_algorithm, "update_realized_power"): + t.prop_algorithm.update_realized_power(realized_percent) + else: + on_time_sec = 0 + off_time_sec = 0 + + return { + "on_time_sec": on_time_sec, + "off_time_sec": off_time_sec, + "on_percent": t.safe_on_percent, + "hvac_mode": str(t.vtherm_hvac_mode), + } + + async def control_heating(self, timestamp=None, force=False): + """TPI-specific control heating logic.""" + del timestamp + t = self._thermostat + + # Feed the Auto TPI manager + if self._auto_tpi_manager: + # 1. Update manager's transient state + await self._auto_tpi_manager.update( + room_temp=t.current_temperature, + ext_temp=t.current_outdoor_temperature, + target_temp=t.target_temperature, + hvac_mode=str(t.vtherm_hvac_mode), + is_overpowering_detected=t.power_manager.is_overpowering_detected, + is_central_boiler_off=self._is_central_boiler_off(), + is_heating_failure=t.heating_failure_detection_manager.is_failure_detected, + ) + + # 2. Synchronize parameters if learning is active + new_params = await self._auto_tpi_manager.calculate() + if self._auto_tpi_manager.learning_active and new_params: + new_coef_int = new_params.get(CONF_TPI_COEF_INT) + new_coef_ext = new_params.get(CONF_TPI_COEF_EXT) + if new_coef_int is not None and new_coef_ext is not None: + if t.prop_algorithm: + # Update effective algo parameters + t.prop_algorithm.update_parameters(tpi_coef_int=new_coef_int, tpi_coef_ext=new_coef_ext) + # Keep thermostat attributes in sync + t.tpi_coef_int = new_coef_int + t.tpi_coef_ext = new_coef_ext + _LOGGER.debug("%s - Synced PropAlgorithm with current Auto TPI coeffs: int=%.3f, ext=%.3f", + t, new_coef_int, new_coef_ext) + + # Stop here if we are off + if t.vtherm_hvac_mode == VThermHvacMode_OFF: + _LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF)", t) + t._on_time_sec = 0 + t._off_time_sec = int(t.cycle_min * 60) + if t.is_device_active: + await t.async_underlying_entity_turn_off() + elif t.cycle_scheduler and t.cycle_scheduler.is_cycle_running: + # The master scheduler may still hold a pending heat cycle while + # the physical device is already in its OFF phase. + await t.cycle_scheduler.cancel_cycle() + else: + on_percent = 0 + if t.prop_algorithm: + on_percent = t.on_percent + if on_percent is None: + # Temperature sensor was not yet available at the last + # recalculate() call (e.g. HA restart before sensor comes + # back online). Preserve the current switch state instead of + # turning it off with on_percent=0 (bug 1884). + _LOGGER.info( + "%s - on_percent is None (temperature unavailable). " "Skipping cycle to preserve current switch state.", + t, + ) + return + + on_time_sec, off_time_sec, forced_by_timing = calculate_cycle_times( + on_percent, + t.cycle_min, + t.minimal_activation_delay, + t.minimal_deactivation_delay, + ) + + realized_percent = on_time_sec / (t.cycle_min * 60) + + if forced_by_timing: + if t.prop_algorithm and hasattr(t.prop_algorithm, "update_realized_power"): + t.prop_algorithm.update_realized_power(realized_percent) + + await t.cycle_scheduler.start_cycle( + t.vtherm_hvac_mode, + on_percent, + force, + ) + + async def on_state_changed(self, changed: bool): + """Handle state changes.""" + del changed + # Cycle management is now passive, no need to start/stop loop + pass + + def update_attributes(self): + """Add TPI-specific attributes to thermostat.""" + t = self._thermostat + t._attr_extra_state_attributes["specific_states"].update({ + "auto_tpi_state": "on" if self._auto_tpi_manager and self._auto_tpi_manager.learning_active else "off", + "auto_tpi_continuous_kext": "on" if self._auto_tpi_manager and self._auto_tpi_manager._continuous_kext else "off", + "auto_tpi_learning": ( + self._auto_tpi_manager.get_filtered_state() + if self._auto_tpi_manager and (self._auto_tpi_manager.learning_active or self._auto_tpi_manager._continuous_kext) + else {} + ), + }) + + t._attr_extra_state_attributes["configuration"].update({ + "minimal_activation_delay_sec": t.minimal_activation_delay, + "minimal_deactivation_delay_sec": t.minimal_deactivation_delay, + }) + + async def _async_update_tpi_config_entry(self): + """Update the config entry with current TPI parameters.""" + t = self._thermostat + entry = t.hass.config_entries.async_get_entry(t.unique_id) + if entry: + new_data = entry.data.copy() + new_data[CONF_TPI_COEF_INT] = t.tpi_coef_int + new_data[CONF_TPI_COEF_EXT] = t.tpi_coef_ext + new_data[CONF_TPI_THRESHOLD_LOW] = t.tpi_threshold_low + new_data[CONF_TPI_THRESHOLD_HIGH] = t.tpi_threshold_high + new_data[CONF_MINIMAL_ACTIVATION_DELAY] = t.minimal_activation_delay + new_data[CONF_MINIMAL_DEACTIVATION_DELAY] = t.minimal_deactivation_delay + + result = t.hass.config_entries.async_update_entry(entry, data=new_data) + _LOGGER.debug("%s - Config entry updated with new TPI params: %s", t, result) + + async def service_set_tpi_parameters( + self, + tpi_coef_int: float | None = None, + tpi_coef_ext: float | None = None, + minimal_activation_delay: int | None = None, + minimal_deactivation_delay: int | None = None, + tpi_threshold_low: float | None = None, + tpi_threshold_high: float | None = None, + ): + """Service handler for set_tpi_parameters.""" + t = self._thermostat + + if t.lock_manager.check_is_locked("service_set_tpi_parameters"): + return + + write_event_log( + _LOGGER, + t, + f"Calling SERVICE_SET_TPI_PARAMETERS, tpi_coef_int: {tpi_coef_int}, " + f"tpi_coef_ext: {tpi_coef_ext}" + f"minimal_activation_delay: {minimal_activation_delay}, " + f"minimal_deactivation_delay: {minimal_deactivation_delay}, " + f"tpi_threshold_low: {tpi_threshold_low}, " + f"tpi_threshold_high: {tpi_threshold_high}", + ) + + if t.prop_algorithm is None: + raise ServiceValidationError(f"{t} - No TPI algorithm configured for this thermostat.") + + entry = t.hass.config_entries.async_get_entry(t.unique_id) + if not entry: + raise ServiceValidationError(f"{t} - No config entry has been found for this thermostat.") + + if entry.data.get(CONF_USE_TPI_CENTRAL_CONFIG, False): + raise ServiceValidationError(f"{t} - Impossible to set TPI parameters when using central TPI configuration.") + + # Update the algorithm with coefficients and thresholds + t.prop_algorithm.update_parameters( + tpi_coef_int, + tpi_coef_ext, + tpi_threshold_low, + tpi_threshold_high, + ) + + # Update thermostat attributes directly (handler updates them via setters) + t.tpi_coef_int = t.prop_algorithm.tpi_coef_int + t.tpi_coef_ext = t.prop_algorithm.tpi_coef_ext + t.tpi_threshold_low = t.prop_algorithm.tpi_threshold_low + t.tpi_threshold_high = t.prop_algorithm.tpi_threshold_high + + # Update delays directly on thermostat (not in algo anymore) + if minimal_activation_delay is not None: + t.minimal_activation_delay = minimal_activation_delay + t.cycle_scheduler.min_activation_delay = t.minimal_activation_delay + if minimal_deactivation_delay is not None: + t.minimal_deactivation_delay = minimal_deactivation_delay + t.cycle_scheduler.min_deactivation_delay = t.minimal_deactivation_delay + + await self._async_update_tpi_config_entry() + + if t.is_removed: + _LOGGER.debug("%s - Entity is removed, stop service_set_tpi_parameters", t) + return + + t.recalculate() + await t.async_control_heating(force=True) + + async def service_set_auto_tpi_mode( + self, + auto_tpi_mode: bool, + reinitialise: bool = True, + allow_kint_boost_on_stagnation: bool = False, + allow_kext_compensation_on_overshoot: bool = False, + ): + """Service handler for set_auto_tpi_mode.""" + t = self._thermostat + + if t.proportional_function != PROPORTIONAL_FUNCTION_TPI: + raise ServiceValidationError(f"{t} - This service is only available for TPI algorithm.") + if not t.entry_infos.get(CONF_AUTO_TPI_MODE, False): + raise ServiceValidationError(f"{t} - Auto TPI is not enabled in configuration.") + + write_event_log( + _LOGGER, + t, + f"Calling SERVICE_SET_AUTO_TPI_MODE, auto_tpi_mode: {auto_tpi_mode}, " + f"reinitialise: {reinitialise}, " + f"allow_kint_boost: {allow_kint_boost_on_stagnation}, " + f"allow_kext_overshoot: {allow_kext_compensation_on_overshoot}", + ) + await self.async_set_auto_tpi_mode( + auto_tpi_mode, + reinitialise, + allow_kint_boost_on_stagnation, + allow_kext_compensation_on_overshoot, + ) + + async def service_auto_tpi_calibrate_capacity( + self, + save_to_config: bool, + min_power_threshold: int, + start_date: datetime | None = None, + end_date: datetime | None = None, + ): + """Service handler for auto_tpi_calibrate_capacity.""" + t = self._thermostat + + if t.proportional_function != PROPORTIONAL_FUNCTION_TPI: + raise ServiceValidationError(f"{t} - This service is only available for TPI algorithm.") + if not t.entry_infos.get(CONF_AUTO_TPI_MODE, False): + raise ServiceValidationError(f"{t} - Auto TPI is not enabled in configuration.") + + write_event_log(_LOGGER, t, f"Calling SERVICE_AUTO_TPI_CALIBRATE_CAPACITY, save_to_config: {save_to_config}, start_date: {start_date}, end_date: {end_date}, min_power_threshold: {min_power_threshold}") + + if not self._auto_tpi_manager: + raise ServiceValidationError(f"{t} - Auto TPI Manager not initialized, cannot calibrate capacity.") + + result = await self._auto_tpi_manager.service_calibrate_capacity( + thermostat_entity_id=t.entity_id, + ext_temp_entity_id=t.ext_temp_sensor_entity_id, + save_to_config=save_to_config, + start_date=start_date, + end_date=end_date, + min_power_threshold=min_power_threshold / 100.0, + ) + + if result and result.get("success") and result.get("max_capacity"): + t.recalculate() + + t.update_custom_attributes() + t.async_write_ha_state() + + return result + + async def async_set_auto_tpi_mode( + self, + auto_tpi_mode: bool, + reinitialise: bool = True, + allow_kint_boost: bool = False, + allow_kext_overshoot: bool = False, + ): + """Set the auto TPI mode.""" + t = self._thermostat + + _LOGGER.debug( + "%s - async_set_auto_tpi_mode called with auto_tpi_mode=%s, reinitialise=%s, kint_boost=%s, kext_overshoot=%s", + t, + auto_tpi_mode, + reinitialise, + allow_kint_boost, + allow_kext_overshoot, + ) + if not self._auto_tpi_manager: + _LOGGER.warning("%s - Auto TPI Manager not initialized", t) + return + + # Safety check: Prevent enabling learning if the feature is disabled in config + if auto_tpi_mode and not t.entry_infos.get(CONF_AUTO_TPI_MODE, False): + _LOGGER.warning("%s - Cannot start Auto TPI Learning: feature is disabled in configuration", t) + await self._auto_tpi_manager.stop_learning() + return + + if auto_tpi_mode: + # Use the original configured default values + await self._auto_tpi_manager.start_learning( + coef_int=self._auto_tpi_manager._default_coef_int, + coef_ext=self._auto_tpi_manager._default_coef_ext, + reset_data=reinitialise, + allow_kint_boost=allow_kint_boost, + allow_kext_overshoot=allow_kext_overshoot, + ) + + # Sync PropAlgorithm with the configured coefficients + if t.prop_algorithm: + t.prop_algorithm.update_parameters(tpi_coef_int=t.tpi_coef_int, tpi_coef_ext=t.tpi_coef_ext) + _LOGGER.info("%s - PropAlgorithm synced with config: Kint=%.3f, Kext=%.3f", + t, t.tpi_coef_int, t.tpi_coef_ext) + + # If we enable auto_tpi, we must disable central config for TPI + # Note: _entry_infos is a dict, we can update it directly + if t._entry_infos: + t._entry_infos[CONF_USE_TPI_CENTRAL_CONFIG] = False + + # Persist the change to the config entry + entry = t.hass.config_entries.async_get_entry(t.unique_id) + if entry and entry.data.get(CONF_USE_TPI_CENTRAL_CONFIG, True): + new_data = entry.data.copy() + new_data[CONF_USE_TPI_CENTRAL_CONFIG] = False + t.hass.config_entries.async_update_entry(entry, data=new_data) + + if t.is_removed: + _LOGGER.debug("%s - Entity is removed, stop async_set_auto_tpi_mode", t) + return + + # Starting learning is sufficient, cycle processing is passive + pass + else: + await self._auto_tpi_manager.stop_learning() + + # Apply configured coefficients to PropAlgorithm + if t.prop_algorithm: + t.prop_algorithm.update_parameters(tpi_coef_int=t.tpi_coef_int, tpi_coef_ext=t.tpi_coef_ext) + _LOGGER.info( + "%s - PropAlgorithm reset to config values: Kint=%.3f, Kext=%.3f", + t, t.tpi_coef_int, t.tpi_coef_ext + ) + + # Fire event to notify listeners + t.hass.bus.async_fire( + AUTO_TPI_EVENT, + { + "entity_id": t.entity_id, + "auto_tpi_mode": self._auto_tpi_manager.learning_active, + }, + ) + + # Force update of state attributes + t.update_custom_attributes() + t.async_write_ha_state() diff --git a/custom_components/versatile_thermostat/select.py b/custom_components/versatile_thermostat/select.py new file mode 100644 index 0000000..a3f65bb --- /dev/null +++ b/custom_components/versatile_thermostat/select.py @@ -0,0 +1,126 @@ +# pylint: disable=unused-argument + +""" Implements the VersatileThermostat select component """ +import logging +from vtherm_api.log_collector import get_vtherm_logger + +from homeassistant.core import HomeAssistant + +from homeassistant.components.select import SelectEntity +from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from custom_components.versatile_thermostat.base_thermostat import ( + ConfigData, +) + +from custom_components.versatile_thermostat.vtherm_central_api import VersatileThermostatAPI + +from .const import ( + DOMAIN, + DEVICE_MANUFACTURER, + CONF_NAME, + CONF_THERMOSTAT_TYPE, + CONF_THERMOSTAT_CENTRAL_CONFIG, + CENTRAL_MODE_AUTO, + CENTRAL_MODES, + overrides, +) +from .commons import write_event_log + +_LOGGER = get_vtherm_logger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the VersatileThermostat selects with config flow.""" + unique_id = entry.entry_id + name = entry.data.get(CONF_NAME) + _LOGGER.debug("%s - Calling async_setup_entry entry=%s, data=%s", name, entry.entry_id, entry.data) + vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) + + if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG: + return + + entities = [ + CentralModeSelect(hass, unique_id, name, entry.data), + ] + + async_add_entities(entities, True) + + +class CentralModeSelect(SelectEntity, RestoreEntity): + """Representation of the central mode choice""" + + def __init__( + self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData + ): + """Initialize the energy sensor""" + self._config_id = unique_id + self._device_name = entry_infos.get(CONF_NAME) + self._attr_name = "Central Mode" + self._attr_unique_id = "central_mode" + self._attr_options = CENTRAL_MODES + self._attr_current_option = CENTRAL_MODE_AUTO + + @property + def icon(self) -> str: + return "mdi:form-select" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + entry_type=None, + identifiers={(DOMAIN, self._config_id)}, + name=self._device_name, + manufacturer=DEVICE_MANUFACTURER, + model=DOMAIN, + ) + + @overrides + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + _LOGGER.debug( + "%s - Calling async_added_to_hass old_state is %s", self, old_state + ) + if old_state is not None: + self._attr_current_option = old_state.state + + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass) + api.register_central_mode_select(self) + + @overrides + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + old_option = self._attr_current_option + + if option == old_option: + return + + if option in CENTRAL_MODES: + write_event_log(_LOGGER, self, f"Central mode is being changed from {old_option} to {option}") + self._attr_current_option = option + await self.notify_central_mode_change(old_central_mode=old_option) + + @overrides + def select_option(self, option: str) -> None: + """Change the selected option""" + # Update the VTherms which have temperature in central config + self.hass.create_task(self.async_select_option(option)) + + async def notify_central_mode_change(self, old_central_mode: str | None = None): + """Notify all VTherm that the central_mode have change""" + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass) + # Update all VTherm states + await api.notify_central_mode_change(old_central_mode) + + def __str__(self) -> str: + return f"VersatileThermostat-{self.name}" diff --git a/custom_components/versatile_thermostat/sensor.py b/custom_components/versatile_thermostat/sensor.py new file mode 100644 index 0000000..9b1a76c --- /dev/null +++ b/custom_components/versatile_thermostat/sensor.py @@ -0,0 +1,1100 @@ +# pylint: disable=unused-argument +""" Implements the VersatileThermostat sensors component """ +import logging +from vtherm_api.log_collector import get_vtherm_logger +import math +from collections.abc import Callable + +from homeassistant.core import HomeAssistant, callback, Event, State + +from homeassistant.const import ( + UnitOfTime, + UnitOfPower, + UnitOfEnergy, + PERCENTAGE, +) + +from homeassistant.components.sensor import ( + SensorEntity, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry + +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.event import ( + async_track_state_change_event, +) + +from homeassistant.components.climate import ( + ClimateEntity, + DOMAIN as CLIMATE_DOMAIN, +) + + +from .base_thermostat import BaseThermostat +from .vtherm_central_api import VersatileThermostatAPI +from .base_entity import VersatileThermostatBaseEntity +from .commons import cleanup_orphan_entity +from .const import ( + DOMAIN, + DEVICE_MANUFACTURER, + CONF_NAME, + CONF_DEVICE_POWER, + CONF_PROP_FUNCTION, + PROPORTIONAL_FUNCTION_TPI, + CONF_THERMOSTAT_SWITCH, + CONF_THERMOSTAT_VALVE, + CONF_THERMOSTAT_CLIMATE, + CONF_THERMOSTAT_TYPE, + CONF_THERMOSTAT_CENTRAL_CONFIG, + CONF_USE_CENTRAL_BOILER_FEATURE, + CONF_AUTO_REGULATION_VALVE, + CONF_AUTO_REGULATION_MODE, + CONF_TPI_COEF_INT, + CONF_TPI_COEF_EXT, + CONF_AUTO_TPI_MODE, + overrides, +) + +THRESHOLD_WATT_KILO = 100 + +_LOGGER = get_vtherm_logger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the VersatileThermostat sensors with config flow.""" + unique_id = entry.entry_id + name = entry.data.get(CONF_NAME) + _LOGGER.debug("%s - Calling async_setup_entry entry=%s, data=%s", name, entry.entry_id, entry.data) + + vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) + have_valve_regulation = ( + entry.data.get(CONF_AUTO_REGULATION_MODE) == CONF_AUTO_REGULATION_VALVE + ) + + entities = None + + if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG: + if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE): + entities = [ + NbActiveDeviceForBoilerSensor(hass, unique_id, name, entry.data), + TotalPowerActiveDeviceForBoilerSensor(hass, unique_id, name, entry.data), + ] + else: + entities = [ + LastTemperatureSensor(hass, unique_id, name, entry.data), + LastExtTemperatureSensor(hass, unique_id, name, entry.data), + TemperatureSlopeSensor(hass, unique_id, name, entry.data), + EMATemperatureSensor(hass, unique_id, name, entry.data), + ] + if entry.data.get(CONF_DEVICE_POWER): + entities.append(EnergySensor(hass, unique_id, name, entry.data)) + if have_valve_regulation or entry.data.get(CONF_THERMOSTAT_TYPE) in [ + CONF_THERMOSTAT_SWITCH, + CONF_THERMOSTAT_VALVE, + ]: + entities.append(MeanPowerSensor(hass, unique_id, name, entry.data)) + + is_tpi_capable = entry.data.get(CONF_THERMOSTAT_TYPE) in [ + CONF_THERMOSTAT_SWITCH, + CONF_THERMOSTAT_VALVE, + ] or ( + entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE + and have_valve_regulation + ) + has_proportional_algorithm = ( + is_tpi_capable and entry.data.get(CONF_PROP_FUNCTION) is not None + ) + + if has_proportional_algorithm: + entities.append(OnPercentSensor(hass, unique_id, name, entry.data)) + 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 + or have_valve_regulation + ): + entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data)) + + if ( + entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE + and not have_valve_regulation + ): + entities.append( + RegulatedTemperatureSensor(hass, unique_id, name, entry.data) + ) + + # Add Auto TPI Sensor only if: + # 1. Thermostat is TPI-capable + # 2. TPI algorithm is selected + # 3. Auto TPI mode is enabled in configuration + should_have_auto_tpi = ( + is_tpi_capable + and entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI + and entry.data.get(CONF_AUTO_TPI_MODE, False) + ) + + if should_have_auto_tpi: + entities.append(AutoTpiSensor(hass, unique_id, name, entry.data)) + else: + # Cleanup: Remove orphan Auto TPI sensor from registry if it exists + await cleanup_orphan_entity(hass, entry, "sensor", name, "auto_tpi_learning") + + if entities: + async_add_entities(entities, True) + + +class EnergySensor(VersatileThermostatBaseEntity, SensorEntity): + """Representation of a Energy sensor which exposes the energy""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the energy sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_name = "Energy" + self._attr_unique_id = f"{self._device_name}_energy" + + @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) + + energy = self.my_climate.total_energy + if energy is None: + return + + if math.isnan(energy) or math.isinf(energy): + raise ValueError(f"Sensor has illegal state {self.my_climate.total_energy}") + + old_state = self._attr_native_value + self._attr_native_value = round(energy, self.suggested_display_precision) + if old_state != self._attr_native_value: + self.async_write_ha_state() + return + + @property + def icon(self) -> str | None: + return "mdi:lightning-bolt" + + @property + def device_class(self) -> SensorDeviceClass | None: + if not self.my_climate: + return None + return SensorDeviceClass.ENERGY + + @property + def state_class(self) -> SensorStateClass | None: + if not self.my_climate: + return None + return SensorStateClass.TOTAL_INCREASING + + @property + def native_unit_of_measurement(self) -> str | None: + if not self.my_climate: + return None + + if self.my_climate.power_manager.device_power > THRESHOLD_WATT_KILO: + return UnitOfEnergy.WATT_HOUR + else: + return UnitOfEnergy.KILO_WATT_HOUR + + @property + def suggested_display_precision(self) -> int | None: + """Return the suggested number of decimal digits for display.""" + return 3 + + +class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity): + """Representation of a power sensor which exposes the mean power in a cycle""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the energy sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_name = "Mean power cycle" + self._attr_unique_id = f"{self._device_name}_mean_power_cycle" + + @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) + + mean_cycle_power = self.my_climate.power_manager.mean_cycle_power + if mean_cycle_power is None: + return + + if math.isnan(float(mean_cycle_power)) or math.isinf(float(mean_cycle_power)): + raise ValueError(f"Sensor has illegal state {mean_cycle_power}") + + mean_cycle_power = float(mean_cycle_power) + + old_state = self._attr_native_value + self._attr_native_value = round( + mean_cycle_power, + self.suggested_display_precision, + ) + if old_state != self._attr_native_value: + self.async_write_ha_state() + return + + @property + def icon(self) -> str | None: + return "mdi:flash-outline" + + @property + def device_class(self) -> SensorDeviceClass | None: + return SensorDeviceClass.POWER + + @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 + + if self.my_climate.power_manager.device_power > THRESHOLD_WATT_KILO: + return UnitOfPower.WATT + else: + return UnitOfPower.KILO_WATT + + @property + def suggested_display_precision(self) -> int | None: + """Return the suggested number of decimal digits for display.""" + return 3 + + +class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity): + """Representation of a on percent sensor which exposes the on_percent in a cycle""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the energy sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_name = "Power percent" + self._attr_unique_id = f"{self._device_name}_power_percent" + + @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) + + raw_on_percent = ( + self.my_climate.proportional_algorithm.on_percent + if self.my_climate + and self.my_climate.has_prop + and self.my_climate.proportional_algorithm is not None + else None + ) + if raw_on_percent is None: + return + on_percent = float(raw_on_percent) + + if math.isnan(on_percent) or math.isinf(on_percent): + raise ValueError(f"Sensor has illegal state {on_percent}") + + old_state = self._attr_native_value + self._attr_native_value = round( + on_percent * 100.0, self.suggested_display_precision + ) + if old_state != self._attr_native_value: + self.async_write_ha_state() + return + + @property + def icon(self) -> str | None: + return "mdi:meter-electric-outline" + + @property + def device_class(self) -> SensorDeviceClass | None: + return SensorDeviceClass.POWER_FACTOR + + @property + def state_class(self) -> SensorStateClass | None: + return SensorStateClass.MEASUREMENT + + @property + def native_unit_of_measurement(self) -> str | None: + return PERCENTAGE + + @property + def suggested_display_precision(self) -> int | None: + """Return the suggested number of decimal digits for display.""" + return 1 + + +class AutoTpiSensor(VersatileThermostatBaseEntity, SensorEntity): + """Representation of the Auto TPI Learning state""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the Auto TPI sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_name = "Auto TPI Learning State" + self._attr_unique_id = f"{self._device_name}_auto_tpi_learning" + self._attr_icon = "mdi:brain" + + @callback + async def async_my_climate_changed(self, event: Event = None): + """Called when my climate have change""" + + # Verify has_prop and proportional_algorithm + # proportional_algorithm can be None during initialization even if has_prop is True + if not self.my_climate or not self.my_climate.has_prop: + self._attr_native_value = "disabled" + self.async_write_ha_state() + return + + if not hasattr(self.my_climate, "auto_tpi_manager") or not self.my_climate.auto_tpi_manager: + self._attr_native_value = "disabled" + self.async_write_ha_state() + return + + manager = self.my_climate.auto_tpi_manager + + # Determine state + if manager.learning_active: + if manager.is_in_bootstrap: + self._attr_native_value = "Bootstrap" + else: + self._attr_native_value = "Active" + else: + self._attr_native_value = "Off" # Or "Completed" / "Idle" depending on context, but "Off" implies not learning. + + # Update attributes + # Update attributes + self._attr_extra_state_attributes = { + "coeff_int_cycles": manager.int_cycles, + "coeff_ext_cycles": manager.ext_cycles, + "heating_cycles_count": manager.heating_cycles_count, # Total heating cycles + "thermal_time_constant_hours": manager.time_constant, # Building's thermal time constant τ + "model_confidence": manager.confidence, # Model confidence (R² or percentage) + "last_learning_status": manager.state.last_learning_status, # Reason for last learning outcome + "learning_start_dt": manager.state.learning_start_date, + "allow_kint_boost_on_stagnation": manager.state.allow_kint_boost, + "allow_kext_compensation_on_overshoot": manager.state.allow_kext_overshoot, + "bootstrap_failure_count": manager.state.bootstrap_failure_count, + } + + # Add mode-specific attributes + if manager._current_hvac_mode == "cool": + self._attr_extra_state_attributes.update({ + "max_capacity_cool": manager.state.max_capacity_cool, + }) + else: + self._attr_extra_state_attributes.update({ + "max_capacity_heat": manager.state.max_capacity_heat, + "capacity_heat_status": "learning" if manager.is_in_bootstrap else "learned", + "capacity_heat_value": manager.state.max_capacity_heat, + "capacity_heat_count": manager.state.capacity_heat_learn_count, + }) + + # Add calculated TPI coefficients + calculated = manager.get_calculated_params() + self._attr_extra_state_attributes.update({ + "calculated_coef_int": calculated.get(CONF_TPI_COEF_INT), + "calculated_coef_ext": calculated.get(CONF_TPI_COEF_EXT), + }) + + self.async_write_ha_state() + + +class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity): + """Representation of a on percent sensor which exposes the on_percent in a cycle""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the energy sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_name = "Valve open percent" + self._attr_unique_id = f"{self._device_name}_valve_open_percent" + + @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) + + if not self.my_climate or not hasattr(self.my_climate, "valve_open_percent"): + _LOGGER.warning("%s - my_climate not found or no valve_open_percent property found. This could be normal at startup. Ignore the underlying device change.", self) + return + + old_state = self._attr_native_value + self._attr_native_value = self.my_climate.valve_open_percent + if old_state != self._attr_native_value: + self.async_write_ha_state() + return + + @property + def icon(self) -> str | None: + return "mdi:pipe-valve" + + @property + def device_class(self) -> SensorDeviceClass | None: + return SensorDeviceClass.POWER_FACTOR + + @property + def state_class(self) -> SensorStateClass | None: + return SensorStateClass.MEASUREMENT + + @property + def native_unit_of_measurement(self) -> str | None: + return PERCENTAGE + + @property + def suggested_display_precision(self) -> int | None: + """Return the suggested number of decimal digits for display.""" + return 0 + + +class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity): + """Representation of a on time sensor which exposes the on_time_sec in a cycle""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the energy sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_name = "On time" + self._attr_unique_id = f"{self._device_name}_on_time" + + @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) + + on_time = ( + float(self.my_climate.on_time_sec) + if self.my_climate and hasattr(self.my_climate, "on_time_sec") + else None + ) + + if on_time is None: + return + + if math.isnan(on_time) or math.isinf(on_time): + raise ValueError(f"Sensor has illegal state {on_time}") + + old_state = self._attr_native_value + self._attr_native_value = round(on_time) + if old_state != self._attr_native_value: + self.async_write_ha_state() + return + + @property + def icon(self) -> str | None: + return "mdi:timer-play" + + @property + def device_class(self) -> SensorDeviceClass | None: + return SensorDeviceClass.DURATION + + @property + def state_class(self) -> SensorStateClass | None: + return SensorStateClass.MEASUREMENT + + @property + def native_unit_of_measurement(self) -> str | None: + return UnitOfTime.SECONDS + + +class OffTimeSensor(VersatileThermostatBaseEntity, SensorEntity): + """Representation of a on time sensor which exposes the off_time_sec in a cycle""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the energy sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_name = "Off time" + self._attr_unique_id = f"{self._device_name}_off_time" + + @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) + + off_time = ( + float(self.my_climate.off_time_sec) + if self.my_climate and hasattr(self.my_climate, "off_time_sec") + else None + ) + if off_time is None: + return + + if math.isnan(off_time) or math.isinf(off_time): + raise ValueError(f"Sensor has illegal state {off_time}") + + old_state = self._attr_native_value + self._attr_native_value = round(off_time) + if old_state != self._attr_native_value: + self.async_write_ha_state() + return + + @property + def icon(self) -> str | None: + return "mdi:timer-off-outline" + + @property + def device_class(self) -> SensorDeviceClass | None: + return SensorDeviceClass.DURATION + + @property + def state_class(self) -> SensorStateClass | None: + return SensorStateClass.MEASUREMENT + + @property + def native_unit_of_measurement(self) -> str | None: + return UnitOfTime.SECONDS + + +class LastTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity): + """Representation of a last temperature datetime sensor""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the last temperature datetime sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_name = "Last temperature date" + self._attr_entity_registry_enabled_default = False + self._attr_unique_id = f"{self._device_name}_last_temp_datetime" + + @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) + + old_state = self._attr_native_value + self._attr_native_value = self.my_climate.last_temperature_measure + if old_state != self._attr_native_value: + self.async_write_ha_state() + return + + @property + def icon(self) -> str | None: + return "mdi:home-clock" + + @property + def device_class(self) -> SensorDeviceClass | None: + return SensorDeviceClass.TIMESTAMP + + +class LastExtTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity): + """Representation of a last external temperature datetime sensor""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the last temperature datetime sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_name = "Last external temperature date" + self._attr_entity_registry_enabled_default = False + self._attr_unique_id = f"{self._device_name}_last_ext_temp_datetime" + + @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) + + old_state = self._attr_native_value + self._attr_native_value = self.my_climate.last_ext_temperature_measure + if old_state != self._attr_native_value: + self.async_write_ha_state() + return + + @property + def icon(self) -> str | None: + return "mdi:sun-clock" + + @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 not isinstance(self._attr_native_value, (int, float)) + 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 + "/hour" + + @property + def suggested_display_precision(self) -> int | None: + """Return the suggested number of decimal digits for display.""" + return 2 + + +class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity): + """Representation of a Energy sensor which exposes the energy""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the regulated temperature sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_name = "Regulated temperature" + self._attr_unique_id = f"{self._device_name}_regulated_temperature" + + @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) + + new_temp = self.my_climate.regulated_target_temp + if new_temp is None: + return + + if math.isnan(new_temp) or math.isinf(new_temp): + raise ValueError(f"Sensor has illegal state {new_temp}") + + old_state = self._attr_native_value + self._attr_native_value = round(new_temp, self.suggested_display_precision) + if old_state != self._attr_native_value: + self.async_write_ha_state() + return + + @property + def icon(self) -> str | None: + return "mdi:thermometer-auto" + + @property + def device_class(self) -> SensorDeviceClass | None: + return SensorDeviceClass.TEMPERATURE + + @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 self.hass.config.units.temperature_unit + return self.my_climate.temperature_unit + + @property + def suggested_display_precision(self) -> int | None: + """Return the suggested number of decimal digits for display.""" + return 1 + + +class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity): + """Representation of a Exponential Moving Average temp""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the regulated temperature sensor""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + self._attr_name = "EMA temperature" + self._attr_unique_id = f"{self._device_name}_ema_temperature" + + @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) + + new_ema = self.my_climate.ema_temperature + if new_ema is None: + return + + if math.isnan(new_ema) or math.isinf(new_ema): + raise ValueError(f"Sensor has illegal state {new_ema}") + + old_state = self._attr_native_value + self._attr_native_value = new_ema + if old_state != self._attr_native_value: + self.async_write_ha_state() + return + + @property + def icon(self) -> str | None: + return "mdi:thermometer-lines" + + @property + def device_class(self) -> SensorDeviceClass | None: + return SensorDeviceClass.TEMPERATURE + + @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 self.hass.config.units.temperature_unit + return self.my_climate.temperature_unit + + @property + def suggested_display_precision(self) -> int | None: + """Return the suggested number of decimal digits for display.""" + return 2 + + +class NbActiveDeviceForBoilerSensor(SensorEntity): + """Representation of the number of VTherm + which are active and configured to activate the boiler""" + + # TODO remove all listener mecanisms + _entity_component_unrecorded_attributes = SensorEntity._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access + frozenset({"active_device_ids"}) + ) + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the energy sensor""" + self._hass = hass + self._config_id = unique_id + self._device_name = entry_infos.get(CONF_NAME) + self._attr_name = "Nb device active for boiler" + self._attr_unique_id = "nb_device_active_boiler" + self._attr_value = self._attr_native_value = None # default value + self._entities = [] + self._attr_active_device_ids = [] # Holds the entity ids of active devices`` + self._cancel_listener_nb_active: Callable | None = None + + @property + def extra_state_attributes(self) -> dict: + """Return additional attributes for the sensor.""" + return { + "active_device_ids": self._attr_active_device_ids, + } + + @property + def icon(self) -> str | None: + return "mdi:heat-wave" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + entry_type=None, + identifiers={(DOMAIN, self._config_id)}, + name=self._device_name, + manufacturer=DEVICE_MANUFACTURER, + model=DOMAIN, + ) + + @property + def state_class(self) -> SensorStateClass | None: + return SensorStateClass.MEASUREMENT + + @property + def suggested_display_precision(self) -> int | None: + """Return the suggested number of decimal digits for display.""" + return 0 + + @overrides + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass) + api.central_boiler_manager.register_nb_device_active_boiler(self) + + async def listen_vtherms_entities(self): + """Initialize the listening of state change of VTherms""" + + # Listen to all VTherm underlying state change + self._entities = [] + # underlying_entities_id = [] + + component: EntityComponent[ClimateEntity] = self.hass.data.get(CLIMATE_DOMAIN) + if component is None: + _LOGGER.warning("%s - No climate component found in hass.data", self) + return + + self.cancel_listening_nb_active() + + for entity in list(component.entities): + if isinstance(entity, BaseThermostat) and entity.is_used_by_central_boiler: + self._entities.append(entity) + # 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é + # self._cancel_listener_nb_active = async_track_state_change_event( + # self._hass, + # underlying_entities_id, + # self.calculate_nb_active_devices, + # ) + # _LOGGER.info( + # "%s - the underlyings that could control the central boiler are %s", + # self, + # underlying_entities_id, + # ) + # Fix 1406 + # self.async_on_remove(self._cancel_listener_nb_active) + # else: + # _LOGGER.debug("%s - no VTherm could control the central boiler", self) + + await self.calculate_nb_active_devices(None) + + async def calculate_nb_active_devices(self, event: Event): + """Calculate the number of active VTherm and the total active power that have an + influence on the central boiler and update the list of active device names.""" + + # _LOGGER.debug("%s- calculate_nb_active_devices_or_power - 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_or_power 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 interesting for us + if ( + old_state is not None + and new_state.state == old_state.state + and new_hvac_action == old_hvac_action + # issue 698 - force recalculation when underlying climate doesn't have any hvac_action + and new_hvac_action is not None + ): + # 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 + active_device_ids = [] + + for entity in self._entities: + device_actives = entity.device_actives + _LOGGER.debug( + "%s - After examining the device_actives, device_actives is %s", + entity.name, + device_actives, + ) + + nb_active += len(device_actives) + active_device_ids.extend(device_actives) + + self._attr_native_value = nb_active + self._attr_active_device_ids = active_device_ids + + self.async_write_ha_state() + await VersatileThermostatAPI.get_vtherm_api(self._hass).central_boiler_manager.refresh_central_boiler_custom_attributes() + + @property + def active_device_ids(self) -> list: + """Get the list of active device id""" + return self._attr_active_device_ids + + def __str__(self): + return f"{self.name}" + + def cancel_listening_nb_active(self): + """Cancel the listening of underlying VTherm state changes""" + if self._cancel_listener_nb_active is not None: + try: + self._cancel_listener_nb_active() # pylint: disable=not-callable + except (ValueError, TypeError): # the listener could be already cancelled + pass + self._cancel_listener_nb_active = None + + @overrides + async def async_will_remove_from_hass(self) -> None: + """Called when entity is about to be removed from hass""" + self.cancel_listening_nb_active() + + +class TotalPowerActiveDeviceForBoilerSensor(NbActiveDeviceForBoilerSensor): + """Representation of the total power of VTherm + which are active and configured to activate the boiler""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the energy sensor""" + super().__init__(hass, unique_id, name, entry_infos) + self._attr_name = "Total power active for boiler" + self._attr_unique_id = "total_power_active_boiler" + self._attr_value = self._attr_native_value = None # default value + self._cancel_listener_power: callable | None = None + + @property + def icon(self) -> str | None: + return "mdi:flash-auto" + + @property + def device_class(self) -> SensorDeviceClass | None: + return SensorDeviceClass.POWER + + @property + def suggested_display_precision(self) -> int | None: + """Return the suggested number of decimal digits for display.""" + return 2 + + @overrides + async def async_added_to_hass(self) -> None: + # do not ! await super().async_added_to_hass() + + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass) + api.central_boiler_manager.register_total_power_active_boiler(self) + + async def listen_vtherms_entities(self): + """Initialize the listening of state change of VTherms""" + + # Listen to all VTherm state change + self._entities = [] + entities_id = [] + + component: EntityComponent[ClimateEntity] = self.hass.data.get(CLIMATE_DOMAIN) + if component is None: + _LOGGER.warning("%s - No climate component found in hass.data", self) + return + + self.cancel_listening_power() + + for entity in list(component.entities): + if isinstance(entity, BaseThermostat) and entity.is_used_by_central_boiler: + self._entities.append(entity) + entities_id.append(entity.entity_id) + + if len(self._entities) > 0: + # Arme l'écoute de la première entité + self._cancel_listener_power = async_track_state_change_event( + self._hass, + entities_id, + self.calculate_total_power, + ) + _LOGGER.info( + "%s - the VTherm that could control the central boiler are %s", + self, + entities_id, + ) + # Fix 1406 + # self.async_on_remove(self._cancel_listener_power) + else: + _LOGGER.debug("%s - no VTherm could control the central boiler", self) + + await self.calculate_total_power(None) + + async def calculate_total_power(self, event: Event): + """Calculate the total active power that have an + influence on the central boiler and update the list of active device names.""" + + # _LOGGER.debug("%s- calculate_nb_active_devices_or_power - 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_or_power 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_mean_cycle_power = new_state.attributes.get("power_manager", {}).get("mean_cycle_power") + old_mean_cycle_power = old_state.attributes.get("power_manager", {}).get("mean_cycle_power") if old_state is not None else None + + # Filter events that are not interesting for us + if old_state is not None and new_mean_cycle_power == old_mean_cycle_power: + # A false state change + return + + _LOGGER.debug( + "%s - calculating the total power of active underlying device for boiler activation. mean_power_cycle change from %s to %s", + self, + old_mean_cycle_power, + new_mean_cycle_power, + ) + else: + _LOGGER.debug( + "%s - calculating the total power of active underlying device for boiler activation. First time calculation", + self, + ) + + total_active_power = 0 + active_device_ids = [] + + for entity in self._entities: + mean_cycle_power = entity.power_manager.mean_cycle_power + if mean_cycle_power is None or mean_cycle_power <= 0: + continue + + _LOGGER.debug( + "%s - After examining the mean_cycle_power, mean_cycle_power is %s", + entity.name, + mean_cycle_power, + ) + + active_device_ids.extend([under.entity_id for under in entity.activable_underlying_entities]) + + total_active_power += mean_cycle_power + + self._attr_native_value = total_active_power + self._attr_active_device_ids = active_device_ids + + self.async_write_ha_state() + await VersatileThermostatAPI.get_vtherm_api(self._hass).central_boiler_manager.refresh_central_boiler_custom_attributes() + + @property + def active_device_ids(self) -> list: + """Get the list of active device id""" + return self._attr_active_device_ids + + def __str__(self): + return f"{self.name}" + + @overrides + async def async_will_remove_from_hass(self) -> None: + """Called when entity is about to be removed from hass""" + self.cancel_listening_power() + + def cancel_listening_power(self): + """Cancel the listening of underlying VTherm state changes""" + if self._cancel_listener_power is not None: + self._cancel_listener_power() + self._cancel_listener_power = None diff --git a/custom_components/versatile_thermostat/services.yaml b/custom_components/versatile_thermostat/services.yaml new file mode 100644 index 0000000..dbde934 --- /dev/null +++ b/custom_components/versatile_thermostat/services.yaml @@ -0,0 +1,446 @@ +reload: + name: Reload + description: Reload all Versatile Thermostat entities. + +set_presence: + name: Set presence + description: Force the presence mode in thermostat + target: + entity: + integration: versatile_thermostat + fields: + presence: + name: Presence + description: Presence setting + required: true + advanced: false + example: "on" + default: "on" + selector: + select: + options: + - "on" + - "off" + - "home" + - "not_home" + +set_safety: + name: Set safety + description: Change the safety parameters + target: + entity: + integration: versatile_thermostat + fields: + delay_min: + name: Delay in minutes + description: Maximum allowed delay in minutes between two temperature mesures + required: false + advanced: false + example: "30" + selector: + number: + min: 0 + max: 9999 + unit_of_measurement: "min" + mode: box + min_on_percent: + name: Minimal on_percent + description: Minimal heating percent value for safety preset activation + required: false + advanced: false + example: "0.5" + default: "0.5" + selector: + number: + min: 0 + max: 1 + step: 0.05 + unit_of_measurement: "%" + mode: slider + default_on_percent: + name: on_percent used in safety mode + description: The default heating percent value in safety preset + required: false + advanced: false + example: "0.1" + default: "0.1" + selector: + number: + min: 0 + max: 1 + step: 0.05 + unit_of_measurement: "%" + mode: slider + +set_window_bypass: + name: Set Window ByPass + description: Bypass the window state to enable heating with window open. + target: + entity: + integration: versatile_thermostat + fields: + window_bypass: + name: Window ByPass + description: ByPass value + required: true + advanced: false + default: true + selector: + boolean: + +set_auto_regulation_mode: + name: Set Auto Regulation mode + description: Change the mode of self-regulation (only for VTherm over climate) + target: + entity: + integration: versatile_thermostat + fields: + auto_regulation_mode: + name: Auto regulation mode + description: Possible values + required: true + advanced: false + default: true + selector: + select: + options: + - "None" + - "Light" + - "Medium" + - "Strong" + - "Slow" + - "Expert" + +set_auto_fan_mode: + name: Set Auto Fan mode + description: Change the mode of auto-fan (only for VTherm over climate) + target: + entity: + integration: versatile_thermostat + fields: + auto_fan_mode: + name: Auto fan mode + description: Possible values + required: true + advanced: false + default: true + selector: + select: + options: + - "None" + - "Low" + - "Medium" + - "High" + - "Turbo" + +set_hvac_mode_sleep: + name: Set HVAC Mode to Sleep + description: Turns a VTherm climate into seasonal sleep - OFF with valve 100% open (only for VTherm over climate with valve regulation). + target: + entity: + integration: versatile_thermostat + +lock: + name: Lock + description: "Locks the thermostat, preventing any changes to its configuration from UI or automations." + target: + entity: + integration: versatile_thermostat + + fields: + code: + name: Lock code + description: The optional lock code + required: false + advanced: false + example: "1234" + selector: + text: + type: password + +unlock: + name: Unlock + description: "Unlocks the thermostat, allowing changes to its configuration." + target: + entity: + integration: versatile_thermostat + fields: + code: + name: Lock code + description: The optional lock code + required: false + advanced: false + example: "1234" + selector: + text: + type: password + +set_tpi_parameters: + name: Set TPI parameters + description: Change the TPI parameters + target: + entity: + integration: versatile_thermostat + fields: + tpi_coef_int: + name: Coef Int + description: Coefficient Integral + required: false + advanced: false + example: "0.6" + selector: + number: + min: 0 + max: 10 + step: 0.01 + mode: box + tpi_coef_ext: + name: Coef Ext + description: Coefficient External + required: false + advanced: false + example: "0.01" + selector: + number: + min: 0 + max: 1 + step: 0.001 + mode: box + minimal_activation_delay: + name: Minimal Activation delay + description: Delay in seconds under which the equipment will not be activated + required: false + advanced: true + example: "10" + selector: + number: + min: 0 + max: 1000 + step: 1 + mode: box + unit_of_measurement: "seconds" + minimal_deactivation_delay: + name: Minimal Deactivation delay + description: Delay in seconds under which the equipment will be kept active + required: false + advanced: true + example: "10" + selector: + number: + min: 0 + max: 1000 + step: 1 + mode: box + unit_of_measurement: "seconds" + tpi_threshold_low: + name: Low TPI threshold + description: Threshold in ° under which the TPI algorithm will be on. 0 means no threshold + required: false + advanced: false + example: "0.1" + selector: + number: + min: -10 + max: 10 + step: 0.1 + mode: box + unit_of_measurement: "°" + tpi_threshold_high: + name: High TPI threshold + description: Threshold in ° above which the TPI algorithm will be off. 0 means no threshold + required: false + advanced: false + example: "0.1" + selector: + number: + min: -10 + max: 10 + step: 0.1 + mode: box + unit_of_measurement: "°" + +set_auto_tpi_mode: + name: Set Auto TPI mode + description: Enable or disable the Auto TPI learning mode. + target: + entity: + integration: versatile_thermostat + domain: + - climate + fields: + auto_tpi_mode: + name: Auto TPI mode + description: Enable (true) or disable (false) the Auto TPI learning + required: true + advanced: false + default: true + selector: + boolean: + reinitialise: + name: Reset learning data + description: Reset all learning data when enabling Auto TPI mode (default is true) + required: false + advanced: false + default: true + selector: + boolean: + allow_kint_boost_on_stagnation: + name: Allow Kint boost + description: Allow boosting Kint when temperature stagnates despite demand (default false) + required: true + advanced: false + default: false + selector: + boolean: + allow_kext_compensation_on_overshoot: + name: Allow Kext compensation + description: Allow adjusting Kext when temperature overshoots (default false) + required: true + advanced: false + default: false + selector: + boolean: + + +auto_tpi_calibrate_capacity: + name: Auto Tpi Calibrate Capacity + description: Calibrate the heating/cooling capacity (in min/°) using linear regression on the entity's history data. + target: + entity: + integration: versatile_thermostat + domain: + - climate + fields: + start_date: + name: History from date + description: Date from which to retrieve history (default is 30 days ago) + required: false + advanced: false + example: "2023-11-01" + selector: + date: + end_date: + name: History to date + description: Date until which to retrieve history (default is now) + required: false + advanced: false + example: "2023-12-01" + selector: + date: + min_power_threshold: + name: Minimum Power Threshold + description: Minimum power percentage to consider a cycle as saturated (default 95%). Lower values (e.g., 90%) will include more cycles but may be less accurate. + required: true + default: 95 + advanced: true + example: 95 + selector: + number: + min: 80 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: slider + save_to_config: + name: Save to Config + description: Whether to apply the calculated capacity to the entity's configuration + required: true + advanced: false + example: true + selector: + boolean: + +set_timed_preset: + name: Set Timed Preset + description: Force a preset for a given duration. After the duration expires, the original preset will be restored. + target: + entity: + integration: versatile_thermostat + domain: + - climate + fields: + preset: + name: Preset + description: The preset to apply temporarily + required: true + advanced: false + example: "boost" + selector: + text: + duration_minutes: + name: Duration (minutes) + description: Duration in minutes for which the preset will be active + required: true + advanced: false + example: 30 + default: 30 + selector: + number: + min: 1 + max: 1440 + step: 1 + unit_of_measurement: "min" + mode: box + +cancel_timed_preset: + name: Cancel Timed Preset + description: Cancel any active timed preset and restore the original preset. + target: + entity: + integration: versatile_thermostat + domain: + - climate + +recalibrate_valves: + name: Recalibrate valves + description: Recalibrate opening/closing degrees for all valve underlyings of a VTherm (only over_climate_valve). + target: + entity: + integration: versatile_thermostat + domain: + - climate + fields: + delay_seconds: + name: Delay seconds + description: Delay in seconds between open and close step for each valve + required: true + example: 5 + default: 60 + selector: + number: + min: 30 + max: 300 + +download_logs: + name: Download logs + description: Collect and download filtered logs for a VTherm entity. + target: + entity: + integration: versatile_thermostat + domain: + - climate + fields: + log_level: + name: Log level + description: Minimum log level to include + required: false + default: "DEBUG" + selector: + select: + options: + - "DEBUG" + - "INFO" + - "WARNING" + - "ERROR" + period_start: + name: Period start + description: Start of the extraction period (default is 60 minutes ago) + required: false + selector: + datetime: + period_end: + name: Period end + description: End of the extraction period (default is now) + required: false + selector: + datetime: diff --git a/custom_components/versatile_thermostat/state_manager.py b/custom_components/versatile_thermostat/state_manager.py new file mode 100644 index 0000000..06354be --- /dev/null +++ b/custom_components/versatile_thermostat/state_manager.py @@ -0,0 +1,303 @@ +"""StateManager for Versatile Thermostat. + +This class manages both the current and the requested state of a VTherm. +""" + +import logging +from vtherm_api.log_collector import get_vtherm_logger +from typing import Optional, TYPE_CHECKING, Any +from .const import ( + HVAC_OFF_REASON_SAFETY, + HVAC_OFF_REASON_MANUAL, + HVAC_OFF_REASON_WINDOW_DETECTION, + HVAC_OFF_REASON_AUTO_START_STOP, + HVAC_OFF_REASON_SLEEP_MODE, + HVAC_OFF_REASON_CENTRAL_MODE, + CONF_WINDOW_ECO_TEMP, + CONF_WINDOW_FAN_ONLY, + CONF_WINDOW_FROST_TEMP, + CONF_WINDOW_TURN_OFF, + CENTRAL_MODE_STOPPED, + CENTRAL_MODE_COOL_ONLY, + CENTRAL_MODE_HEAT_ONLY, + CENTRAL_MODE_FROST_PROTECTION, + ATTR_CURRENT_STATE, + ATTR_REQUESTED_STATE, + MSG_TARGET_TEMP_POWER, + MSG_TARGET_TEMP_WINDOW_ECO, + MSG_TARGET_TEMP_WINDOW_FROST, + MSG_TARGET_TEMP_CENTRAL_MODE, + MSG_TARGET_TEMP_ACTIVITY_DETECTED, + MSG_TARGET_TEMP_ACTIVITY_NOT_DETECTED, + MSG_TARGET_TEMP_ABSENCE_DETECTED, + MSG_TARGET_TEMP_TIMED_PRESET, +) +from .vtherm_state import VThermState +from .vtherm_hvac_mode import VThermHvacMode_OFF, VThermHvacMode_FAN_ONLY, VThermHvacMode_COOL, VThermHvacMode_HEAT, VThermHvacMode_SLEEP +from .vtherm_preset import VThermPreset + +_LOGGER = get_vtherm_logger(__name__) + +if TYPE_CHECKING: + from .base_thermostat import BaseThermostat + +class StateManager: + """Manages the current and requested state for a VTherm. + + Attributes: + current_state: The actual state of the thermostat. + requested_state: The desired/requested state to be applied. + """ + + def __init__(self): + """Initialize the StateManager with empty initial states. + """ + self._current_state = VThermState(hvac_mode=VThermHvacMode_OFF, preset=VThermPreset.NONE) + self._requested_state = VThermState(hvac_mode=VThermHvacMode_OFF, preset=VThermPreset.NONE) + + @property + def current_state(self) -> Optional[VThermState]: + """Get or set the current state.""" + return self._current_state + + @property + def requested_state(self) -> Optional[VThermState]: + """Get or set the requested state.""" + return self._requested_state + + async def calculate_current_state(self, vtherm: "BaseThermostat") -> bool: + """Calculate and update the current state from the given base_thermostat. + + Args: + vtherm: The thermostat object to use for calculation. + + Returns: + bool: True or False according to rules to be defined later. + """ + if not vtherm: + return False + + vtherm.set_temperature_reason(None) + change_hvac_mode = await self.calculate_current_hvac_mode(vtherm) + change_preset = await self.calculate_current_preset(vtherm) + change_target_temp = await self.calculate_current_target_temperature(vtherm) + + return change_hvac_mode or change_preset or change_target_temp + + async def calculate_current_hvac_mode(self, vtherm: "BaseThermostat") -> bool: + """Calculate and update the current HVAC mode from the given base_thermostat. + + - check if safety manager is detected has an impact on hvac_mode + - if not check if window manager has an impact on hvac_mode + - if not check if auto start/stop manager has an impact on hvac_mode + - else set hvac_mode to requested_state.hvac_mode + + then publish an event if hvac_mode has changed + + Args: + vtherm: The thermostat object to use for calculation. + + Returns: + bool: True or False according to preceeding rules + + """ + if not vtherm: + return False + + # Implement HVAC mode calculation logic here + # overpowering never change the hvac_mode + # if vtherm.power_manager.is_overpowering_detected: + + # First check safety + if vtherm.last_central_mode == CENTRAL_MODE_STOPPED: + self._current_state.set_hvac_mode(VThermHvacMode_OFF) + vtherm.set_hvac_off_reason(HVAC_OFF_REASON_CENTRAL_MODE) + + elif vtherm.safety_manager.is_safety_detected and (vtherm.is_over_climate or vtherm.safety_manager.safety_default_on_percent <= 0.0): + self._current_state.set_hvac_mode(VThermHvacMode_OFF) + vtherm.set_hvac_off_reason(HVAC_OFF_REASON_SAFETY) + + # then check if window is open + elif vtherm.window_manager.is_window_detected and self._requested_state.hvac_mode != VThermHvacMode_OFF: + if vtherm.window_manager.window_action == CONF_WINDOW_FAN_ONLY and VThermHvacMode_FAN_ONLY in vtherm.vtherm_hvac_modes: + self._current_state.set_hvac_mode(VThermHvacMode_FAN_ONLY) + elif vtherm.window_manager.window_action == CONF_WINDOW_TURN_OFF or ( + vtherm.window_manager.window_action == CONF_WINDOW_FAN_ONLY and VThermHvacMode_FAN_ONLY not in vtherm.vtherm_hvac_modes + ): # default is to turn_off + self._current_state.set_hvac_mode(VThermHvacMode_OFF) + vtherm.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION) + + elif vtherm.auto_start_stop_manager and vtherm.auto_start_stop_manager.is_auto_stop_detected and self._requested_state.hvac_mode != VThermHvacMode_OFF: + self._current_state.set_hvac_mode(VThermHvacMode_OFF) + vtherm.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP) + + elif vtherm.last_central_mode == CENTRAL_MODE_COOL_ONLY and self._requested_state.hvac_mode != VThermHvacMode_OFF: + if VThermHvacMode_COOL in vtherm.vtherm_hvac_modes: + self._current_state.set_hvac_mode(VThermHvacMode_COOL) + else: + vtherm.set_hvac_off_reason(HVAC_OFF_REASON_CENTRAL_MODE) + self._current_state.set_hvac_mode(VThermHvacMode_OFF) + + elif vtherm.last_central_mode == CENTRAL_MODE_HEAT_ONLY and self._requested_state.hvac_mode != VThermHvacMode_OFF: + if VThermHvacMode_HEAT in vtherm.vtherm_hvac_modes: + self._current_state.set_hvac_mode(VThermHvacMode_HEAT) + else: + vtherm.set_hvac_off_reason(HVAC_OFF_REASON_CENTRAL_MODE) + self._current_state.set_hvac_mode(VThermHvacMode_OFF) + + elif vtherm.last_central_mode == CENTRAL_MODE_FROST_PROTECTION and self._requested_state.hvac_mode != VThermHvacMode_OFF: + preset_modes = vtherm.vtherm_preset_modes + if preset_modes is None or VThermPreset.FROST not in preset_modes or VThermHvacMode_HEAT not in vtherm.vtherm_hvac_modes: + self._current_state.set_hvac_mode(VThermHvacMode_OFF) + vtherm.set_hvac_off_reason(HVAC_OFF_REASON_CENTRAL_MODE) + elif vtherm.vtherm_hvac_mode != VThermHvacMode_HEAT and VThermHvacMode_HEAT in vtherm.vtherm_hvac_modes: + self._current_state.set_hvac_mode(VThermHvacMode_HEAT) + + # all is fine set current_state = requested_state + else: + if self._current_state.hvac_mode == VThermHvacMode_OFF and self._requested_state.hvac_mode == VThermHvacMode_OFF: + _LOGGER.info("%s - already in OFF. Change the reason to MANUAL", vtherm) + vtherm.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL if not vtherm.is_sleeping else HVAC_OFF_REASON_SLEEP_MODE) + + self._current_state.set_hvac_mode(self._requested_state.hvac_mode) + + # Calculate hvac_off_reason + if self._current_state.hvac_mode not in [VThermHvacMode_OFF, VThermHvacMode_SLEEP] and vtherm.hvac_off_reason is not None: + vtherm.set_hvac_off_reason(None) + elif self._current_state.hvac_mode == VThermHvacMode_SLEEP: + vtherm.set_hvac_off_reason(HVAC_OFF_REASON_SLEEP_MODE) + + return self._current_state.is_hvac_mode_changed + + async def calculate_current_preset(self, vtherm: "BaseThermostat") -> bool: + """Calculate and update the current preset state from the given base_thermostat. + + - check if power manager is detected has an impact on preset + - if not check if safety manager has an impact on preset + - if not check if timed preset manager has a timed preset active + - else set preset to requested_state.preset + + Send an event if preset has changed + + Args: + vtherm: The thermostat object to use for calculation. + + Returns: + bool: True or False according to rules to be preceeding rules + """ + + # check overpowering first + if vtherm.power_manager.is_overpowering_detected and self._current_state.hvac_mode != VThermHvacMode_OFF: + # turn off underlying and take the hvac_mode + self._current_state.set_preset(VThermPreset.POWER) + vtherm.set_temperature_reason(MSG_TARGET_TEMP_POWER) + + # then check safety + elif vtherm.safety_manager.is_safety_detected and self._current_state.hvac_mode != VThermHvacMode_OFF: + self._current_state.set_preset(VThermPreset.SAFETY) + + elif vtherm.last_central_mode == CENTRAL_MODE_FROST_PROTECTION: + preset_modes = vtherm.vtherm_preset_modes + if preset_modes is not None and VThermPreset.FROST in preset_modes and vtherm.vtherm_hvac_mode == VThermHvacMode_HEAT: + self._current_state.set_preset(VThermPreset.FROST) + vtherm.set_temperature_reason(MSG_TARGET_TEMP_CENTRAL_MODE) + + # then check if a timed preset is active + elif vtherm.timed_preset_manager.is_timed_preset_active and self._current_state.hvac_mode != VThermHvacMode_OFF: + timed_preset = vtherm.timed_preset_manager.timed_preset + if timed_preset: + self._current_state.set_preset(timed_preset) + vtherm.set_temperature_reason(MSG_TARGET_TEMP_TIMED_PRESET) + + # all is fine set current_state = requested_state + else: + self._current_state.set_preset(self._requested_state.preset) + + return self._current_state.is_preset_changed + + async def calculate_current_target_temperature(self, vtherm: "BaseThermostat") -> bool: + """Calculate and update the current target temperature from the given base_thermostat. + + - check if window manager is detected has an impact on target temperature + - if not check if presence manager has an impact on target temperature + - if not check if motion manager has an impact on target temperature + - else set target temperature to requested_state.target_temperature + + Send an event if target temperature has changed + + Args: + vtherm: The thermostat object to use for calculation. + + Returns: + bool: True or False according to rules to be preceeding rules + """ + + updated = False + window_action = vtherm.window_manager.window_action + + # Handle window action correction for COOL mode (Issue #1987) + if vtherm.vtherm_hvac_mode == VThermHvacMode_COOL and window_action == CONF_WINDOW_FROST_TEMP: + _LOGGER.debug( + "%s - HVAC mode is COOL and window action is Frost, falling back to Eco", + vtherm, + ) + window_action = CONF_WINDOW_ECO_TEMP + + # note that window_manager.is_window_detected is False if bypass is on (so no need to test it here) + if vtherm.window_manager.is_window_detected: + if (window_action == CONF_WINDOW_FROST_TEMP and vtherm.is_preset_configured(VThermPreset.FROST)) or ( + window_action == CONF_WINDOW_ECO_TEMP and vtherm.is_preset_configured(VThermPreset.ECO) + ): + self._current_state.set_target_temperature(vtherm.find_preset_temp(VThermPreset.ECO if window_action == CONF_WINDOW_ECO_TEMP else VThermPreset.FROST)) + vtherm.set_temperature_reason(MSG_TARGET_TEMP_WINDOW_ECO if window_action == CONF_WINDOW_ECO_TEMP else MSG_TARGET_TEMP_WINDOW_FROST) + updated = True + + elif vtherm.motion_manager.is_configured and self._current_state.preset == VThermPreset.ACTIVITY: + new_preset = vtherm.motion_manager.get_current_motion_preset() + _LOGGER.debug("%s - motion will set new target preset: %s", self, new_preset) + self._current_state.set_target_temperature(vtherm.find_preset_temp(new_preset)) + vtherm.set_temperature_reason(MSG_TARGET_TEMP_ACTIVITY_DETECTED if vtherm.motion_manager.is_motion_detected else MSG_TARGET_TEMP_ACTIVITY_NOT_DETECTED) + updated = True + + elif vtherm.presence_manager.is_absence_detected: + if vtherm.vtherm_preset_mode != VThermPreset.NONE: + new_temp = vtherm.find_preset_temp(vtherm.vtherm_preset_mode) + _LOGGER.debug("%s - presence will set new target temperature: %.2f", self, new_temp) + self._current_state.set_target_temperature(new_temp) + vtherm.set_temperature_reason(MSG_TARGET_TEMP_ABSENCE_DETECTED) + updated = True + + if not updated: + # calculate the temperature of the preset + self.update_current_temp_from_requested(vtherm) + + # update requested state temperature to set it in concordance with preset + # 1379 - do not overwrite requested_target_temp to keep the last manual tempe. + # if self._requested_state.preset != VThermPreset.NONE: + # self._requested_state.set_target_temperature(vtherm.find_preset_temp(self._requested_state.preset)) + + return self._current_state.is_target_temperature_changed + + def update_current_temp_from_requested(self, vtherm: "BaseThermostat"): + """Update the current temperature from the requested state preset if any.""" + if self._current_state.preset == VThermPreset.SAFETY: + self._current_state.set_target_temperature(vtherm.find_preset_temp(self._requested_state.preset)) + elif self._current_state.preset != VThermPreset.NONE: + self._current_state.set_target_temperature(vtherm.find_preset_temp(self._current_state.preset)) + elif self._requested_state.target_temperature is not None: + self._current_state.set_target_temperature(self._requested_state.target_temperature) + else: + # affect the min or max temp according to is_ac + if vtherm.ac_mode: + self._current_state.set_target_temperature(vtherm.max_temp) + else: + self._current_state.set_target_temperature(vtherm.min_temp) + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): + """Add some custom attributes""" + extra_state_attributes.update( + { + ATTR_CURRENT_STATE: self.current_state.to_dict(), + ATTR_REQUESTED_STATE: self.requested_state.to_dict(), + } + ) diff --git a/custom_components/versatile_thermostat/strings.json b/custom_components/versatile_thermostat/strings.json new file mode 100644 index 0000000..9b635c9 --- /dev/null +++ b/custom_components/versatile_thermostat/strings.json @@ -0,0 +1,1049 @@ +{ + "title": "Versatile Thermostat configuration", + "config": { + "flow_title": "Versatile Thermostat configuration", + "step": { + "user": { + "title": "Type of Versatile Thermostat", + "description": "Choose thermostat type you want to create", + "data": { + "thermostat_type": "Thermostat type" + }, + "data_description": { + "thermostat_type": "Only one central configuration type is possible" + } + }, + "menu": { + "title": "Menu", + "description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.", + "menu_options": { + "main": "Main attributes", + "central_boiler": "Central boiler", + "type": "Underlyings", + "tpi": "TPI parameters", + "features": "Features", + "presets": "Presets", + "window": "Window detection", + "motion": "Motion detection", + "power": "Power management", + "presence": "Presence detection", + "advanced": "Advanced parameters", + "auto_start_stop": "Auto start and stop", + "valve_regulation": "Valve regulation configuration", + "sync_device_internal_temp": "Synchronize device temperature", + "lock": "Lock", + "heating_failure_detection": "Heating failure detection", + "auto_tpi_configuration": "Auto TPI - Configuration", + "auto_tpi_avg_settings": "Auto TPI - Weighted Average", + "auto_tpi_ema_settings": "Auto TPI - EWMA", + "finalize": "All done", + "configuration_not_complete": "Configuration not complete" + } + }, + "lock": { + "title": "Lock management", + "description": "The Lock feature prevents changes to a thermostat's configuration from the UI or automations while keeping the thermostat operational.", + "data": { + "use_lock_central_config": "Use central lock configuration", + "lock_code": "Optional pincode (4 digits)", + "lock_users": "Lock will prevent commands from Users", + "lock_automations": "Lock will prevent commands from Automations & integrations (i.e scheduler)", + "auto_relock_sec": "Auto-relock delay" + }, + "data_description": { + "use_lock_central_config": "Check to use the central lock configuration. Uncheck to use a specific lock configuration for this VTherm", + "auto_relock_sec": "Delay in seconds before automatically re-locking after an unlock. Set to 0 to disable auto-relock. Default is 30 seconds." + } + }, + "main": { + "title": "Add new Versatile Thermostat", + "description": "Main mandatory attributes", + "data": { + "name": "Name", + "thermostat_type": "Thermostat type", + "temperature_sensor_entity_id": "Room temperature", + "last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime", + "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", + "cycle_min": "Cycle duration (minutes)", + "temp_min": "Minimum temperature allowed", + "temp_max": "Maximum temperature allowed", + "step_temperature": "Temperature step", + "device_power": "Device power", + "use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.", + "use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).", + "used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler" + }, + "data_description": { + "cycle_min": "Minimum duration of one heating/cooling cycle in minutes to avoid too frequent switching", + "temp_min": "Minimum temperature allowed for the thermostat", + "temp_max": "Maximum temperature allowed for the thermostat", + "step_temperature": "Temperature step for setting the target temperature", + "temperature_sensor_entity_id": "Room temperature sensor entity id", + "last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor", + "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" + } + }, + "features": { + "title": "Features", + "description": "Thermostat features", + "data": { + "use_window_feature": "Use window detection", + "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 selecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page", + "use_auto_start_stop_feature": "Use the auto start and stop feature", + "use_heating_failure_detection_feature": "Use heating failure detection" + } + }, + "type": { + "title": "Linked entities", + "description": "Linked entities attributes", + "data": { + "underlying_entity_ids": "The device(s) to be controlled", + "heater_keep_alive": "Switch keep-alive interval in seconds", + "proportional_function": "Algorithm", + "ac_mode": "AC mode", + "sync_device_internal_temp": "Synchronize device internal temperature", + "auto_regulation_mode": "Self-regulation", + "auto_regulation_dtemp": "Regulation threshold", + "auto_regulation_periode_min": "Regulation minimum period", + "auto_regulation_use_device_temp": "Use internal temperature of the underlying", + "inverse_switch_command": "Inverse switch command", + "auto_fan_mode": "Auto fan mode", + "on_command_text": "Turn on command customization", + "vswitch_on_command": "Optional turn on commands", + "off_command_text": "Turn off command customization", + "vswitch_off_command": "Optional turn off commands" + }, + "data_description": { + "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 (other algorithms can be installed as {plugins_link})", + "ac_mode": "Use the Air Conditioning (AC) mode", + "sync_device_internal_temp": "Check to synchronize the internal temperature of the underlying device (fill the new menu item 'Synchronize device temperature' to configure it)", + "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", + "auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation (not needed in most cases)", + "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", + "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary", + "on_command_text": "For underlying of type `select` or `climate` you have to customize the commands." + } + }, + "tpi": { + "title": "TPI", + "description": "Time Proportional Integral attributes", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "tpi_threshold_low": "Low TPI threshold", + "tpi_threshold_high": "High TPI threshold", + "use_tpi_central_config": "Use central TPI configuration", + "minimal_activation_delay": "Minimum activation delay", + "minimal_deactivation_delay": "Minimum deactivation delay", + "auto_tpi_mode": "Enable Auto TPI Learning" + }, + "data_description": { + "tpi_coef_int": "Coefficient to use for internal temperature delta", + "tpi_coef_ext": "Coefficient to use for external temperature delta", + "tpi_threshold_low": "Threshold in ° under which the TPI algorithm will be on. 0 means no threshold", + "tpi_threshold_high": "Threshold in ° above which the TPI algorithm will be off. 0 means no threshold", + "use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm", + "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", + "minimal_deactivation_delay": "Delay in seconds under which the equipment will be kept active", + "auto_tpi_mode": "TPI learning session are started manually, read the doc first." + } + }, + "presets": { + "title": "Presets", + "description": "Select if the thermostat will use central preset - deselect for the thermostat to have its own presets", + "data": { + "use_presets_central_config": "Use central presets configuration" + } + }, + "window": { + "title": "Window management", + "description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease", + "data": { + "window_sensor_entity_id": "Window sensor entity id", + "window_delay": "Window sensor 'on' delay (seconds)", + "window_off_delay": "Window sensor 'off' delay (seconds)", + "window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)", + "window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)", + "window_auto_max_duration": "Maximum duration of automatic window open detection (in min)", + "use_window_central_config": "Use central window configuration", + "window_action": "Action" + }, + "data_description": { + "window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection", + "window_delay": "The delay in seconds before sensor 'on' detection is taken into account", + "window_off_delay": "The delay in seconds before sensor 'off' detection is taken into account. Leave it empty to use the same value as window on delay", + "window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used", + "window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used", + "window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used", + "use_window_central_config": "Select to use the central window configuration. Deselect to use a specific window configuration for this VTherm", + "window_action": "Action to perform if window is deteted as open" + } + }, + "motion": { + "title": "Motion management", + "description": "Motion sensor management. Preset can switch automatically depending on motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name", + "data": { + "motion_sensor_entity_id": "Motion sensor entity id", + "motion_delay": "Activation delay", + "motion_off_delay": "Deactivation delay", + "motion_preset": "Motion preset", + "no_motion_preset": "No motion preset", + "use_motion_central_config": "Use central motion configuration" + }, + "data_description": { + "motion_sensor_entity_id": "The entity id of the motion sensor", + "motion_delay": "Motion activation delay (seconds)", + "motion_off_delay": "Motion deactivation delay (seconds)", + "motion_preset": "Preset to use when motion is detected", + "no_motion_preset": "Preset to use when no motion is detected", + "use_motion_central_config": "Check to use the central motion configuration. Uncheck to use a specific motion configuration for this VTherm" + } + }, + "power": { + "title": "Power management", + "description": "Power management attributes.\nGives the power and max power sensor of your home.\nSpecify the power consumption of the heater when on.\nAll sensors and device power should use the same unit (kW or W)", + "data": { + "power_sensor_entity_id": "Power", + "max_power_sensor_entity_id": "Max power", + "power_temp": "Shedding temperature", + "use_power_central_config": "Use central power configuration" + }, + "data_description": { + "power_sensor_entity_id": "Power sensor entity id", + "max_power_sensor_entity_id": "Max power sensor entity id", + "power_temp": "Temperature for Power shedding", + "use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm" + } + }, + "presence": { + "title": "Presence management", + "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting", + "data": { + "presence_sensor_entity_id": "Presence sensor", + "use_presence_central_config": "Use central presence temperature configuration. Deselect to use specific temperature entities" + }, + "data_description": { + "presence_sensor_entity_id": "Presence sensor entity id" + } + }, + "advanced": { + "title": "Advanced parameters", + "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation", + "data": { + "safety_delay_min": "Safety delay (in minutes)", + "safety_min_on_percent": "Minimum power percent to enable safety mode", + "safety_default_on_percent": "Power percent to use in safety mode", + "repair_incorrect_state": "Repair incorrect state", + "use_advanced_central_config": "Use central advanced configuration" + }, + "data_description": { + "safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state", + "safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset", + "safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", + "repair_incorrect_state": "Automatically repair incorrect state of underlying entities when detected. The feature will re-send the desired command if the underlying device state doesn't match the expected state.", + "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. 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", + "central_boiler_activation_delay_sec": "Activation delay (seconds)", + "keep_alive_boiler_delay_sec": "Keep-alive delay (seconds)" + }, + "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]", + "central_boiler_activation_delay_sec": "Delay in seconds before activating the central boiler when it needs to be turned on", + "keep_alive_boiler_delay_sec": "Delay in seconds to periodically resend the boiler command if keep-alive is enabled. Set to 0 to disable keep-alive." + } + }, + "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", + "opening_threshold_degree": "Opening threshold degree", + "min_opening_degrees": "Min opening degrees", + "max_opening_degrees": "Max opening degrees", + "max_closing_degree": "Max closing degree" + }, + "data_description": { + "offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV has 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 has the entity for better regulation. There should be one per underlying climate entities", + "proportional_function": "Algorithm to use (other algorithms can be installed as {plugins_link})", + "opening_threshold_degree": "Valve opening value under which the valve should be considered as closed (and then 'max_closing_degree' will apply)", + "min_opening_degrees": "Opening degree minimum value for each underlying device when previous threshold is exceeded, comma separated. Default to 0. Example: 20, 25, 30", + "max_opening_degrees": "Opening degree maximum value. The valve will never open more than this value", + "max_closing_degree": "Closing degree maximum value. The valve will never be closed above this value. Set it to 100 to fully close the valve" + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - Configuration", + "description": "General settings for Auto TPI learning.", + "data": { + "auto_tpi_learning_type": "Learning Type", + "heater_heating_time": "Heating time (min)", + "heater_cooling_time": "Cooling time (min)", + "auto_tpi_heating_rate": "Heating Rate ({unit}/h)", + "auto_tpi_aggressiveness": "Aggressiveness", + "auto_tpi_enable_advanced_settings": "Enable Advanced Parameters", + "auto_tpi_continuous_kext": "Continuous Kext Learning" + }, + "data_description": { + "auto_tpi_learning_type": "Choose 'Discovery' for initial startup (Weighted Average, weight 1) or 'Fine Tuning' for continuous adjustments (EWMA, Alpha 0.08).", + "heater_heating_time": "Time to reach full power (minutes)", + "heater_cooling_time": "Time to cool down after stop (minutes)\n\n| Type | Heating time | Cooling time |\n| :--- | :--- | :--- |\n| Electric radiator | 5mn | 7mn |\n| Water radiator | 15mn | 20mn |\n| Underfloor heating | 30mn | 45mn |", + "auto_tpi_heating_rate": "Radiator temperature rise capacity ({unit} per hour). Leave at 0 for automatic learning (bootstrap).", + "auto_tpi_aggressiveness": "Reduction factor for learned coefficients (50-100%). Lower values result in more conservative coefficients and reduce overshoot risk.", + "auto_tpi_enable_advanced_settings": "Check to access parameter fine-tuning (Alpha, Decay, Weight).", + "auto_tpi_continuous_kext": "Check to enable continuous learning of the external coefficient (Kext) outside of AutoTPI sessions." + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - Weighted Average", + "description": "Parameters for Weighted Average method.", + "data": { + "auto_tpi_avg_initial_weight": "Initial Weight" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Weight of the initial/previous value in the average calculation" + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA", + "description": "Parameters for EWMA method.\n\nRecommendations:\n| Situation | Alpha (ema_alpha) | Decay Rate (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Initial Learning | 0.15 | 0.08 |\n| Fine Learning | 0.08 | 0.12 |\n| Continuous Learning | 0.05 | 0.02 |", + "data": { + "auto_tpi_ema_alpha": "Alpha", + "auto_tpi_ema_decay_rate": "Decay Rate", + "auto_tpi_continuous_kext_alpha": "Continuous Kext Alpha" + }, + "data_description": { + "auto_tpi_ema_alpha": "Smoothing factor (0-1). Higher = faster adaptation", + "auto_tpi_ema_decay_rate": "Rate at which Alpha decreases over time (stabilization)", + "auto_tpi_continuous_kext_alpha": "Smoothing factor for continuous Kext learning (Alpha). Default 0.04 (approx 3-5 days adaptation)." + } + }, + "sync_device_internal_temp": { + "title": "Synchronize device internal temperature", + "description": "Configuration to synchronize the internal temperature of the underlying devices with selected entities", + "data": { + "sync_with_calibration": "Apply offset calibration", + "sync_entity_ids": "Entities id used for synchronization" + }, + "data_description": { + "sync_with_calibration": "Check to apply the offset calibration when synchronizing the internal temperature. Uncheck to directly copy the temperature from the selected entities", + "sync_entity_ids": "The list of the entities used to synchronize the internal temperature of the underlying devices. There should be one per underlying device. Should be a calibration entity if checkbox is selected or a temperature entity otherwise. Both are `number` entities." + } + }, + "heating_failure_detection": { + "title": "Heating failure detection", + "description": "Configure the heating failure detection feature. This detects anomalies when heating is expected but temperature doesn't increase, or when heating is off but temperature keeps rising.", + "data": { + "use_heating_failure_detection_feature": "Enable heating failure detection", + "use_heating_failure_detection_central_config": "Use central heating failure detection configuration", + "heating_failure_threshold": "Heating failure threshold", + "cooling_failure_threshold": "Cooling failure threshold", + "heating_failure_detection_delay": "Detection delay (minutes)", + "temperature_change_tolerance": "Temperature change tolerance (°C)", + "failure_detection_enable_template": "Detection enable template" + }, + "data_description": { + "use_heating_failure_detection_feature": "Enable the detection of heating anomalies for VTherms with TPI", + "use_heating_failure_detection_central_config": "Check to use the central heating failure detection configuration. Uncheck to use specific parameters for this VTherm", + "heating_failure_threshold": "On percent threshold above which heating should cause temperature increase (0.9 = 90%)", + "cooling_failure_threshold": "On percent threshold below which temperature should not increase (0.0 = 0%)", + "heating_failure_detection_delay": "Time in minutes to wait before checking if temperature has changed as expected", + "temperature_change_tolerance": "Minimum temperature change in degrees to consider significant. Smaller changes are ignored to filter sensor noise (default: 0.5°C)", + "failure_detection_enable_template": "An optional Jinja2 template that must return True to enable detection. Useful to temporarily disable detection when an external heat source is active (e.g., `is_state('binary_sensor.wood_stove', 'off')` in double curly braces). If empty, detection is always enabled." + } + } + }, + "error": { + "unknown": "Unexpected error", + "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", + "valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings", + "sync_device_internal_temp_nb_entities_incorrect": "The number of entities for synchronize device internal temperature should be equal to the number of underlyings", + "min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30", + "max_opening_degrees_format": "A comma separated list of positive integer between 0 and the max valve opening is expected. Example: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "For each underlying device, max_opening_degrees must be strictly greater than min_opening_degrees. Please check your configuration.", + "vswitch_configuration_incorrect": "The command customization configuration is incorrect. It is required for underlying entities that are not switches, and the format must be service_name[/attribute:value]. More information is available in the README" + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "options": { + "flow_title": "Versatile Thermostat configuration", + "step": { + "user": { + "title": "Type - {name}", + "description": "Choose thermostat type you want to create", + "data": { + "thermostat_type": "Thermostat type" + }, + "data_description": { + "thermostat_type": "Only one central configuration type is possible" + } + }, + "menu": { + "title": "Menu", + "description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.", + "menu_options": { + "main": "Main attributes", + "central_boiler": "Central boiler", + "type": "Underlyings", + "tpi": "TPI parameters", + "features": "Features", + "presets": "Presets", + "window": "Window detection", + "motion": "Motion detection", + "power": "Power management", + "presence": "Presence detection", + "advanced": "Advanced parameters", + "auto_start_stop": "Auto start and stop", + "valve_regulation": "Valve regulation configuration", + "sync_device_internal_temp": "Synchronize device temperature", + "lock": "Lock", + "heating_failure_detection": "Heating failure detection", + "finalize": "All done", + "configuration_not_complete": "Configuration not complete" + } + }, + "lock": { + "title": "Lock management - {name}", + "description": "The Lock feature prevents changes to a thermostat's configuration from the UI or automations while keeping the thermostat operational.", + "data": { + "use_lock_central_config": "Use central lock configuration", + "lock_code": "Optional pincode (4 digits)", + "lock_users": "Lock will prevent commands from Users", + "lock_automations": "Lock will prevent commands from Automations & integrations (i.e scheduler)", + "auto_relock_sec": "Auto-relock delay" + }, + "data_description": { + "use_lock_central_config": "Check to use the central lock configuration. Uncheck to use a specific lock configuration for this VTherm", + "auto_relock_sec": "Delay in seconds before automatically re-locking after an unlock. Set to 0 to disable auto-relock. Default is 30 seconds." + } + }, + "main": { + "title": "Main - {name}", + "description": "Main mandatory attributes", + "data": { + "name": "Name", + "thermostat_type": "Thermostat type", + "temperature_sensor_entity_id": "Room temperature", + "last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime", + "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", + "cycle_min": "Cycle duration (minutes)", + "temp_min": "Minimum temperature allowed", + "temp_max": "Maximum temperature allowed", + "step_temperature": "Temperature step", + "device_power": "Device power", + "use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.", + "use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).", + "used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler" + }, + "data_description": { + "cycle_min": "Minimum duration of one heating/cooling cycle in minutes to avoid too frequent switching", + "temp_min": "Minimum temperature allowed for the thermostat", + "temp_max": "Maximum temperature allowed for the thermostat", + "step_temperature": "Temperature step for setting the target temperature", + "temperature_sensor_entity_id": "Room temperature sensor entity id", + "last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor", + "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" + } + }, + "features": { + "title": "Features - {name}", + "description": "Thermostat features", + "data": { + "use_window_feature": "Use window detection", + "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 selecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page", + "use_auto_start_stop_feature": "Use the auto start and stop feature", + "use_heating_failure_detection_feature": "Use heating failure detection" + } + }, + "type": { + "title": "Entities - {name}", + "description": "Linked entities attributes", + "data": { + "underlying_entity_ids": "The device(s) to be controlled", + "heater_keep_alive": "Switch keep-alive interval in seconds", + "proportional_function": "Algorithm", + "ac_mode": "AC mode", + "sync_device_internal_temp": "Synchronize device temperature", + "auto_regulation_mode": "Self-regulation", + "auto_regulation_dtemp": "Regulation threshold", + "auto_regulation_periode_min": "Regulation minimum period", + "auto_regulation_use_device_temp": "Use internal temperature of the underlying", + "inverse_switch_command": "Inverse switch command", + "auto_fan_mode": "Auto fan mode", + "on_command_text": "Turn on command customization", + "vswitch_on_command": "Optional turn on commands", + "off_command_text": "Turn off command customization", + "vswitch_off_command": "Optional turn off commands" + }, + "data_description": { + "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 (other algorithms can be installed as {plugins_link})", + "ac_mode": "Use the Air Conditioning (AC) mode", + "sync_device_internal_temp": "Check to synchronize the internal temperature of the underlying device (fill the new menu item 'Synchronize device temperature' to configure it)", + "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", + "auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation (not needed in most cases)", + "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", + "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary", + "on_command_text": "For underlying of type `select` or `climate` you have to customize the commands." + } + }, + "tpi": { + "title": "TPI - {name}", + "description": "Time Proportional Integral attributes", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "tpi_threshold_low": "Low TPI threshold", + "tpi_threshold_high": "High TPI threshold", + "use_tpi_central_config": "Use central TPI configuration", + "minimal_activation_delay": "Minimum activation delay", + "minimal_deactivation_delay": "Minimum deactivation delay", + "auto_tpi_mode": "Enable Auto TPI Learning" + }, + "data_description": { + "tpi_coef_int": "Coefficient to use for internal temperature delta", + "tpi_coef_ext": "Coefficient to use for external temperature delta", + "tpi_threshold_low": "Threshold in ° under which the TPI algorithm will be on. 0 means no threshold", + "tpi_threshold_high": "Threshold in ° above which the TPI algorithm will be off. 0 means no threshold", + "use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm", + "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", + "minimal_deactivation_delay": "Delay in seconds under which the equipment will be kept active", + "auto_tpi_mode": "TPI learning session are started manually, read the doc first." + } + }, + "presets": { + "title": "Presets - {name}", + "description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities", + "data": { + "use_presets_central_config": "Use central presets configuration" + } + }, + "window": { + "title": "Window - {name}", + "description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease", + "data": { + "window_sensor_entity_id": "Window sensor entity id", + "window_delay": "Window sensor 'on' delay (seconds)", + "window_off_delay": "Window sensor 'off' delay (seconds)", + "window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)", + "window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)", + "window_auto_max_duration": "Maximum duration of automatic window open detection (in min)", + "use_window_central_config": "Use central window configuration", + "window_action": "Action" + }, + "data_description": { + "window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection", + "window_delay": "The delay in seconds before sensor 'on' detection is taken into account", + "window_off_delay": "The delay in seconds before sensor 'off' detection is taken into account. Leave it empty to use the same value as window on delay", + "window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used", + "window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used", + "window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used", + "use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm", + "window_action": "Action to do if window is deteted as open" + } + }, + "motion": { + "title": "Motion - {name}", + "description": "Motion management. Preset can switch automatically depending of a motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name", + "data": { + "motion_sensor_entity_id": "Motion sensor entity id", + "motion_delay": "Activation delay", + "motion_off_delay": "Deactivation delay", + "motion_preset": "Motion preset", + "no_motion_preset": "No motion preset", + "use_motion_central_config": "Use central motion configuration" + }, + "data_description": { + "motion_sensor_entity_id": "The entity id of the motion sensor", + "motion_delay": "Motion activation delay (seconds)", + "motion_off_delay": "Motion deactivation delay (seconds)", + "motion_preset": "Preset to use when motion is detected", + "no_motion_preset": "Preset to use when no motion is detected", + "use_motion_central_config": "Check to use the central motion configuration. Uncheck to use a specific motion configuration for this VTherm" + } + }, + "power": { + "title": "Power - {name}", + "description": "Power management attributes.\nGives the power and max power sensor of your home.\nSpecify the power consumption of the heater when on.\nAll sensors and device power should use the same unit (kW or W)", + "data": { + "power_sensor_entity_id": "Power", + "max_power_sensor_entity_id": "Max power", + "power_temp": "Shedding temperature", + "use_power_central_config": "Use central power configuration" + }, + "data_description": { + "power_sensor_entity_id": "Power sensor entity id", + "max_power_sensor_entity_id": "Max power sensor entity id", + "power_temp": "Temperature for Power shedding", + "use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm" + } + }, + "presence": { + "title": "Presence - {name}", + "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting", + "data": { + "presence_sensor_entity_id": "Presence sensor", + "use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities" + }, + "data_description": { + "presence_sensor_entity_id": "Presence sensor entity id" + } + }, + "advanced": { + "title": "Advanced - {name}", + "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation", + "data": { + "minimal_activation_delay": "Minimum activation delay", + "minimal_deactivation_delay": "Minimum deactivation delay", + "safety_delay_min": "Safety delay (in minutes)", + "safety_min_on_percent": "Minimum power percent to enable safety mode", + "safety_default_on_percent": "Power percent to use in safety mode", + "repair_incorrect_state": "Repair incorrect state", + "use_advanced_central_config": "Use central advanced configuration" + }, + "data_description": { + "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", + "minimal_deactivation_delay": "Delay in seconds under which the equipment will be kept active", + "safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state", + "safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset", + "safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", + "repair_incorrect_state": "Automatically repair incorrect state of underlying entities when detected. The feature will re-send the desired command if the underlying device state doesn't match the expected state.", + "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. 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", + "central_boiler_activation_delay_sec": "Activation delay (seconds)", + "keep_alive_boiler_delay_sec": "Keep-alive delay (seconds)" + }, + "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]", + "central_boiler_activation_delay_sec": "Delay in seconds before activating the central boiler when it needs to be turned on", + "keep_alive_boiler_delay_sec": "Delay in seconds to periodically resend the boiler command if keep-alive is enabled. Set to 0 to disable keep-alive." + } + }, + "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", + "opening_threshold_degree": "Opening threshold degree", + "min_opening_degrees": "Min opening degrees", + "max_opening_degrees": "Max opening degrees", + "max_closing_degree": "Max closing degree" + }, + "data_description": { + "offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV has 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 has the entity for better regulation. There should be one per underlying climate entities", + "proportional_function": "Algorithm to use (other algorithms can be installed as {plugins_link})", + "opening_threshold_degree": "Valve opening value under which the valve should be considered as closed (and then 'max_closing_degree' will apply)", + "min_opening_degrees": "Opening degree minimum value for each underlying device when previous threshold is exceeded, comma separated. Default to 0. Example: 20, 25, 30", + "max_opening_degrees": "Opening degree maximum value. The valve will never open more than this value", + "max_closing_degree": "Closing degree maximum value. The valve will never be closed above this value. Set it to 100 to fully close the valve" + } + }, + "sync_device_internal_temp": { + "title": "Synchronize device internal temperature", + "description": "Configuration to synchronize the internal temperature of the underlying devices with selected entities", + "data": { + "sync_with_calibration": "Apply offset calibration", + "sync_entity_ids": "Entities id used for synchronization" + }, + "data_description": { + "sync_with_calibration": "Check to apply the offset calibration when synchronizing the internal temperature. Uncheck to directly copy the temperature from the selected entities", + "sync_entity_ids": "The list of the entities used to synchronize the internal temperature of the underlying devices. There should be one per underlying device. Should be a calibration entity if checkbox is selected or a temperature entity otherwise. Both are `number` entities." + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - Configuration - {name}", + "description": "General settings for Auto TPI learning.", + "data": { + "auto_tpi_learning_type": "Learning Type", + "auto_tpi_heating_rate": "Heating Rate ({unit}/h)", + "heater_heating_time": "Heating time (min)", + "heater_cooling_time": "Cooling time (min)", + "auto_tpi_aggressiveness": "Aggressiveness", + "auto_tpi_enable_advanced_settings": "Enable Advanced Parameters", + "auto_tpi_continuous_kext": "Continuous Kext Learning" + }, + "data_description": { + "auto_tpi_learning_type": "Choose 'Discovery' for initial startup (Weighted Average, weight 1) or 'Fine Tuning' for continuous adjustments (EWMA, Alpha 0.08).", + "auto_tpi_heating_rate": "Radiator temperature rise capacity ({unit} per hour). Leave at 0 for automatic learning (bootstrap).", + "heater_heating_time": "Time to reach full power (minutes)", + "heater_cooling_time": "Time to cool down after stop (minutes)\n\n| Type | Heating time | Cooling time |\n| :--- | :--- | :--- |\n| Electric radiator | 5mn | 7mn |\n| Water radiator | 15mn | 20mn |\n| Underfloor heating | 30mn | 45mn |", + "auto_tpi_aggressiveness": "Reduction factor for learned coefficients (50-100%). Lower values result in more conservative coefficients and reduce overshoot risk.", + "auto_tpi_enable_advanced_settings": "Check to modify the parameters of the chosen algorithm.", + "auto_tpi_continuous_kext": "Check to enable continuous learning of the external coefficient (Kext) outside of AutoTPI sessions." + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - Weighted Average - {name}", + "description": "Parameters for Weighted Average method.", + "data": { + "auto_tpi_avg_initial_weight": "Initial Weight" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Initial weight of existing coefficients in the average calculation (1-50 is typical). Higher weight means slower and more stable learning." + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA - {name}", + "description": "Parameters for EWMA method.\n\nRecommendations:\n| Situation | Alpha (ema_alpha) | Decay Rate (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Strong adjustments | 0.15 | 0.08 |\n| Fine tuning | 0.08 | 0.12 |", + "data": { + "auto_tpi_ema_alpha": "Alpha", + "auto_tpi_ema_decay_rate": "Decay Rate", + "auto_tpi_continuous_kext_alpha": "Continuous Kext Alpha" + }, + "data_description": { + "auto_tpi_ema_alpha": "Smoothing factor (0-1). Higher = faster adaptation", + "auto_tpi_ema_decay_rate": "Rate at which Alpha decreases over time (stabilization)", + "auto_tpi_continuous_kext_alpha": "Smoothing factor for continuous Kext learning (Alpha). Default 0.04 (approx 3-5 days adaptation)." + } + }, + "heating_failure_detection": { + "title": "Heating failure detection - {name}", + "description": "Configure the heating failure detection feature. This detects anomalies when heating is expected but temperature doesn't increase, or when heating is off but temperature keeps rising.", + "data": { + "use_heating_failure_detection_feature": "Enable heating failure detection", + "use_heating_failure_detection_central_config": "Use central heating failure detection configuration", + "heating_failure_threshold": "Heating failure threshold", + "cooling_failure_threshold": "Cooling failure threshold", + "heating_failure_detection_delay": "Detection delay (minutes)", + "temperature_change_tolerance": "Temperature change tolerance (°C)", + "failure_detection_enable_template": "Detection enable template" + }, + "data_description": { + "use_heating_failure_detection_feature": "Enable the detection of heating anomalies for VTherms with TPI", + "use_heating_failure_detection_central_config": "Check to use the central heating failure detection configuration. Uncheck to use specific parameters for this VTherm", + "heating_failure_threshold": "On percent threshold above which heating should cause temperature increase (0.9 = 90%)", + "cooling_failure_threshold": "On percent threshold below which temperature should not increase (0.0 = 0%)", + "heating_failure_detection_delay": "Time in minutes to wait before checking if temperature has changed as expected", + "temperature_change_tolerance": "Minimum temperature change in degrees to consider significant. Smaller changes are ignored to filter sensor noise (default: 0.5°C)", + "failure_detection_enable_template": "An optional Jinja2 template that must return True to enable detection. Useful to temporarily disable detection when an external heat source is active (e.g., `is_state('binary_sensor.wood_stove', 'off')` in double curly braces). If empty, detection is always enabled." + } + } + }, + "error": { + "unknown": "Unexpected error", + "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", + "valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings", + "sync_device_internal_temp_nb_entities_incorrect": "The number of entities for synchronize device internal temperature should be equal to the number of underlyings", + "min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30", + "max_opening_degrees_format": "A comma separated list of positive integer between 0 and the max value of the valve is expected. Example: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "For each underlying device, max_opening_degrees must be strictly greater than min_opening_degrees. Please check your configuration.", + "vswitch_configuration_incorrect": "The command customization configuration is incorrect. It is required for underlying entities that are not switches, and the format must be service_name[/attribute:value]. More information is available in the README" + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "selector": { + "thermostat_type": { + "options": { + "thermostat_central_config": "Central configuration", + "thermostat_over_switch": "Thermostat over a switch", + "thermostat_over_climate": "Thermostat over a climate", + "thermostat_over_valve": "Thermostat over a valve" + } + }, + "auto_regulation_mode": { + "options": { + "auto_regulation_slow": "Slow", + "auto_regulation_strong": "Strong", + "auto_regulation_medium": "Medium", + "auto_regulation_light": "Light", + "auto_regulation_expert": "Expert", + "auto_regulation_none": "No auto-regulation", + "auto_regulation_valve": "Direct control of valve" + } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "No auto fan", + "auto_fan_low": "Low", + "auto_fan_medium": "Medium", + "auto_fan_high": "High", + "auto_fan_turbo": "Turbo" + } + }, + "window_action": { + "options": { + "window_turn_off": "Turn off", + "window_fan_only": "Fan only", + "window_frost_temp": "Frost protect", + "window_eco_temp": "Eco" + } + }, + "presets": { + "options": { + "frost": "Frost protect", + "eco": "Eco", + "comfort": "Comfort", + "boost": "Boost" + } + }, + "auto_start_stop": { + "options": { + "auto_start_stop_none": "No auto start/stop", + "auto_start_stop_very_slow": "Very slow detection", + "auto_start_stop_slow": "Slow detection", + "auto_start_stop_medium": "Medium detection", + "auto_start_stop_fast": "Fast detection" + } + }, + "auto_tpi_calculation_method": { + "options": { + "average": "Average", + "ema": "Exponential Moving Average (EMA)" + } + }, + "auto_tpi_learning_type": { + "options": { + "discovery": "Discovery", + "fine_tuning": "Fine Tuning" + } + } + }, + "entity": { + "climate": { + "versatile_thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "power": "Shedding", + "safety": "Safety", + "none": "Manual", + "frost": "Frost" + } + } + } + } + }, + "binary_sensor": { + "safety_state": { + "name": "Safety state" + }, + "heating_failure_state": { + "name": "Heating failure state" + }, + "overpowering_state": { + "name": "Overpowering state" + }, + "window_state": { + "name": "Window state" + }, + "motion_state": { + "name": "Motion state" + }, + "presence_state": { + "name": "Presence state" + }, + "window_bypass_state": { + "name": "Window bypass" + }, + "central_boiler_state": { + "name": "Central boiler" + } + }, + "number": { + "frost_temp": { + "name": "Frost" + }, + "eco_temp": { + "name": "Eco" + }, + "comfort_temp": { + "name": "Comfort" + }, + "boost_temp": { + "name": "Boost" + }, + "frost_ac_temp": { + "name": "Frost ac" + }, + "eco_ac_temp": { + "name": "Eco ac" + }, + "comfort_ac_temp": { + "name": "Comfort ac" + }, + "boost_ac_temp": { + "name": "Boost ac" + }, + "frost_away_temp": { + "name": "Frost away" + }, + "eco_away_temp": { + "name": "Eco away" + }, + "comfort_away_temp": { + "name": "Comfort away" + }, + "boost_away_temp": { + "name": "Boost away" + }, + "eco_ac_away_temp": { + "name": "Eco ac away" + }, + "comfort_ac_away_temp": { + "name": "Comfort ac away" + }, + "boost_ac_away_temp": { + "name": "Boost ac away" + } + } + }, + "services": { + "set_auto_tpi_mode": { + "name": "Set Auto TPI mode", + "description": "Enable or disable the Auto TPI learning mode", + "fields": { + "auto_tpi_mode": { + "name": "Auto TPI mode", + "description": "Enable (true) or disable (false) the Auto TPI learning" + }, + "reinitialise": { + "name": "Reset learning data", + "description": "Reset all learning data when enabling Auto TPI mode (default is true)" + } + } + }, + "reset_auto_tpi_capacities": { + "name": "Reset Auto TPI Capacities", + "description": "Reset the learned maximum heating and cooling capacities to 1.0 °C/h. Learned coefficients are NOT reset." + }, + "auto_tpi_calibrate_capacity": { + "name": "Auto Tpi Calibrate Capacity", + "description": "Calibrate the heating/cooling capacity (in °C/hour) using linear regression on the entity's history data. Learned coefficients are NOT updated.", + "fields": { + "start_date": { + "name": "History from date", + "description": "Date from which to retrieve history (default is 30 days ago)" + }, + "end_date": { + "name": "History to date", + "description": "Date until which to retrieve history (default is now)" + }, + "hvac_mode": { + "name": "HVAC Mode", + "description": "Which mode to calibrate (heat or cool)." + }, + "save_to_config": { + "name": "Save to Config", + "description": "Apply the calculated capacity to the Auto TPI configuration." + }, + "min_power_threshold": { + "name": "Minimum Power Threshold", + "description": "Minimum power percentage to consider a cycle as saturated (default 95%). Lower values will include more cycles but may be less accurate." + } + } + }, + "set_tpi_parameters": { + "name": "Set TPI parameters", + "description": "Change the TPI parameters", + "fields": { + "minimal_activation_delay": { + "name": "Minimal Activation delay", + "description": "Delay in seconds under which the equipment will not be activated" + }, + "minimal_deactivation_delay": { + "name": "Minimal Deactivation delay", + "description": "Delay in seconds under which the equipment will be kept active" + }, + "tpi_threshold_low": { + "name": "Low TPI threshold", + "description": "Threshold in ° under which the TPI algorithm will be on. 0 means no threshold" + }, + "tpi_threshold_high": { + "name": "High TPI threshold", + "description": "Threshold in ° above which the TPI algorithm will be off. 0 means no threshold" + } + } + }, + "lock": { + "name": "Lock", + "description": "Locks the thermostat, preventing any changes to its configuration from UI or automations.", + "fields": { + "code": { + "name": "Lock code", + "description": "The optional lock code" + } + } + }, + "unlock": { + "name": "Unlock", + "description": "Unlocks the thermostat, allowing changes to its configuration.", + "fields": { + "code": { + "name": "Lock code", + "description": "The optional lock code" + } + } + }, + "download_logs": { + "name": "Download logs", + "description": "Collect and download filtered logs for a VTherm entity.", + "fields": { + "log_level": { + "name": "Log level", + "description": "Minimum log level to include" + }, + "period_start": { + "name": "Period start", + "description": "Start of the extraction period" + }, + "period_end": { + "name": "Period end", + "description": "End of the extraction period" + } + } + } + }, + "exceptions": { + "auto_tpi_learning_stopped": { + "message": "Auto TPI learning for {name} has been stopped due to 3 consecutive failures. Reason: {reason}. Please check your configuration." + } + } +} \ No newline at end of file diff --git a/custom_components/versatile_thermostat/switch.py b/custom_components/versatile_thermostat/switch.py new file mode 100644 index 0000000..cd2eb44 --- /dev/null +++ b/custom_components/versatile_thermostat/switch.py @@ -0,0 +1,173 @@ +## pylint: disable=unused-argument + +""" Implements the VersatileThermostat select component """ +import logging +from vtherm_api.log_collector import get_vtherm_logger +from typing import Any + +from homeassistant.core import HomeAssistant, callback + +from homeassistant.const import EntityCategory +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 .base_entity import VersatileThermostatBaseEntity + +from .const import * # pylint: disable=unused-wildcard-import,wildcard-import + +_LOGGER = get_vtherm_logger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the VersatileThermostat switches with config flow.""" + unique_id = entry.entry_id + name = entry.data.get(CONF_NAME) + _LOGGER.debug("%s - Calling async_setup_entry entry=%s, data=%s", name, entry.entry_id, entry.data) + + 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._attr_entity_category = EntityCategory.CONFIG + 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 + + await self.update_my_state_and_vtherm() + + async 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 + and self.my_climate.auto_start_stop_manager is not None + ): + await self.my_climate.auto_start_stop_manager.set_auto_start_stop_enable(self._attr_is_on) + + @callback + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + self._attr_is_on = True + await self.update_my_state_and_vtherm() + + @callback + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + self._attr_is_on = False + await self.update_my_state_and_vtherm() + + @overrides + def turn_off(self, **kwargs: Any): + self.hass.create_task(self.async_turn_off(**kwargs)) + + @overrides + def turn_on(self, **kwargs: Any): + self.hass.create_task(self.async_turn_on(**kwargs)) + + +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 + self._attr_entity_category = EntityCategory.CONFIG + + @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() diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py new file mode 100644 index 0000000..4ae50a0 --- /dev/null +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -0,0 +1,1299 @@ +# pylint: disable=line-too-long, too-many-lines, abstract-method +""" A climate over climate classe """ +import logging +from vtherm_api.log_collector import get_vtherm_logger +from typing import Optional + +from datetime import timedelta, datetime + +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval, EventStateChangedData, async_call_later +from homeassistant.components.climate import ( + HVACAction, + ClimateEntityFeature, +) + +from .commons import round_to_nearest, write_event_log +from .base_thermostat import BaseThermostat, ConfigData +from .pi_algorithm import PITemperatureRegulator + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import + +from .vtherm_central_api import VersatileThermostatAPI +from .underlyings import UnderlyingClimate +from .feature_auto_start_stop_manager import FeatureAutoStartStopManager +from .vtherm_hvac_mode import VThermHvacMode + +_LOGGER = get_vtherm_logger(__name__) + +HVAC_ACTION_ON = [ # pylint: disable=invalid-name + HVACAction.COOLING, + HVACAction.DRYING, + HVACAction.FAN, + HVACAction.HEATING, +] + +class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): + """Representation of a base class for a Versatile Thermostat over a climate""" + + _entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access + frozenset( + { + "is_over_climate", + "vtherm_over_climate", + } + ).union(FeatureAutoStartStopManager.unrecorded_attributes) + ) + + def __init__( + 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._follow_underlying_temp_change: bool = False + self._last_regulation_change = None # NowClass.get_now(hass) + self._sync_entity_list: list[str] = [] + self._sync_with_calibration: bool = False + + # 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_hvac_mode = None + + @overrides + def post_init(self, config_entry: ConfigData): + """Initialize the Thermostat""" + + self._auto_start_stop_manager: FeatureAutoStartStopManager = ( + FeatureAutoStartStopManager(self, self._hass) + ) + + self.register_manager(self._auto_start_stop_manager) + + 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._sync_entity_list = config_entry.get(CONF_SYNC_ENTITY_LIST, []) if config_entry.get(CONF_SYNC_DEVICE_INTERNAL_TEMP, False) else [] + self._sync_with_calibration = config_entry.get(CONF_SYNC_WITH_CALIBRATION, False) + + 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 + ) + + @property + def is_over_climate(self) -> bool: + """True if the Thermostat is over_climate""" + return True + + @overrides + def calculate_hvac_action(self, under_list: list = None) -> 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 + if under_list is None: + under_list = self._underlyings + + one_idle = False + for under in under_list: + if (action := under.hvac_action) not in [ + HVACAction.IDLE, + HVACAction.OFF, + ]: + self._attr_hvac_action = action + return + if under.hvac_action == HVACAction.IDLE: + one_idle = True + if one_idle: + self._attr_hvac_action = HVACAction.IDLE + else: + self._attr_hvac_action = HVACAction.OFF + + async def _send_regulated_temperature(self, force=False): + """Sends the regulated temperature to all underlying""" + + self.stop_recalculate_later() + + if self.vtherm_hvac_mode == VThermHvacMode_OFF: + _LOGGER.debug( + "%s - don't send regulated temperature cause VTherm is off ", self + ) + # In this case, reset the timer of last regulation change to avoid time delta too high + self._last_regulation_change = self.now + return + + if self.target_temperature is None: + _LOGGER.warning( + "%s - don't send regulated temperature cause VTherm target_temp (%s) is None. This should be a temporary warning message.", + self, + self.target_temperature, + ) + return + + _LOGGER.info( + "%s - Calling ThermostatClimate._send_regulated_temperature force=%s", + self, + force, + ) + + if not force and not self.check_auto_regulation_period_min(self.now): + self.do_send_regulated_temp_later() + return + + self._regulation_algo.set_target_temp(self.target_temperature) + + if not self._regulated_target_temp: + self._regulated_target_temp = self.target_temperature + + _LOGGER.info("%s - regulation calculation will be done", self) + + # use _attr_target_temperature_step to round value if _auto_regulation_dtemp is equal to 0 + regulation_step = self._auto_regulation_dtemp if self._auto_regulation_dtemp else self._attr_target_temperature_step + _LOGGER.debug("%s - usage regulation_step: %.2f ", self, regulation_step) + + # Find time delta since last regulation change + time_delta: float = ( + (self.now - self._last_regulation_change).total_seconds() / 60.0 / self._auto_regulation_period_min + if self._last_regulation_change and self._auto_regulation_period_min + else 1.0 + ) + _LOGGER.debug("%s - usage time_delta: %.2f ", self, time_delta) + + if self.current_temperature is not None: + new_regulated_temp = round_to_nearest( + self._regulation_algo.calculate_regulated_temperature(self.current_temperature, self._cur_ext_temp, time_delta), + regulation_step, + ) + else: + new_regulated_temp = self.target_temperature + + _LOGGER.info( + "%s - Regulated temp have changed to %.1f. Resend it to underlyings", + self, + new_regulated_temp, + ) + + self._last_regulation_change = self.now + for under in self._underlyings: + # issue 348 - use device temperature if configured as offset + offset_temp = 0 + device_temp = 0 + if ( + # current_temperature is set + self.current_temperature is not None + # regulation can use the device_temp + 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 + ): + offset_temp = device_temp - self.current_temperature + + target_temp = round_to_nearest(new_regulated_temp + offset_temp, regulation_step) + + # The dtemp is the difference between the new target temp and the last sent temperature to the underlying. + # If the dtemp is too low, we consider that there is no need to send a new temperature to the underlying because it + # will not have any effect on the device. This avoid to send too many temperature changes to the underlying. + dtemp = target_temp - (under.last_sent_temperature if under.last_sent_temperature else 0) + + if not force and abs(dtemp) < (self._auto_regulation_dtemp or 0): + _LOGGER.info( + "%s - dtemp (%.1f) is < %.1f -> forget the regulation send for %s", + self, + dtemp, + self._auto_regulation_dtemp, + under.entity_id, + ) + continue + + _LOGGER.debug( + "%s - The device offset temp for regulation is %.2f - internal temp is %.2f. New target is %.2f", + self, + offset_temp, + device_temp, + target_temp, + ) + + await under.set_temperature( + target_temp, + self._attr_max_temp, + self._attr_min_temp, + ) + + # Update regulated_target_temp after the loop to avoid affecting dtemp calculation for other underlyings + self._regulated_target_temp = new_regulated_temp + + def do_send_regulated_temp_later(self): + """A utility function to set the temperature later on an underlying""" + # For over climate we do nothing because the temperature is set in the main loop + _LOGGER.debug("%s - do_set_temperature_later call", self) + + async def callback_send_regulated_temp(_): + """Callback to send the regulated temperature""" + await self._send_regulated_temperature() + self.update_custom_attributes() + self.async_write_ha_state() + + self.stop_recalculate_later() + + self._cancel_recalculate_later = async_call_later(self._hass, 20, callback_send_regulated_temp) + + def check_auto_regulation_period_min(self, now): + """Check if minimal auto_regulation period is exceeded + Returns true if it is not exceeded (so auto regulation can continue)""" + if self._last_regulation_change is None: + return True + + period = float((now - self._last_regulation_change).total_seconds()) / 60.0 + if period < (self._auto_regulation_period_min or 0): + _LOGGER.info( + "%s - period (%.1f) min is < %.0f min -> forget the auto-regulation send", + self, + period, + self._auto_regulation_period_min, + ) + return False + + _LOGGER.debug( + "%s - period (%.1f) min is >= %.0f min -> auto-regulation is available", + self, + period, + self._auto_regulation_period_min, + ) + return True + + async def _send_auto_fan_mode(self): + """Send the fan mode if auto_fan_mode and temperature gap is > threshold""" + if not self._auto_fan_mode or not self._auto_activated_fan_mode: + return + + dtemp = ( + self.regulated_target_temp if self.is_regulated else self.target_temperature + ) + if dtemp is None or self.current_temperature is None: + return + + dtemp = dtemp - self.current_temperature + should_activate_auto_fan = ( + dtemp >= AUTO_FAN_DTEMP_THRESHOLD or dtemp <= -AUTO_FAN_DTEMP_THRESHOLD + ) + + # deal with ac / non ac mode + hvac_mode = self.vtherm_hvac_mode + if (hvac_mode == VThermHvacMode_COOL and dtemp > 0) or (hvac_mode == VThermHvacMode_HEAT and dtemp < 0) or (hvac_mode == VThermHvacMode_OFF): + should_activate_auto_fan = False + + if should_activate_auto_fan and self.fan_mode != self._auto_activated_fan_mode: + _LOGGER.info( + "%s - Activate the auto fan mode with %s because delta temp is %.2f", + self, + self._auto_fan_mode, + dtemp, + ) + await self.async_set_fan_mode(self._auto_activated_fan_mode) + if ( + not should_activate_auto_fan + and self.fan_mode not in AUTO_FAN_DEACTIVATED_MODES + ): + _LOGGER.info( + "%s - DeActivate the auto fan mode with %s because delta temp is %.2f", + self, + self._auto_deactivated_fan_mode, + dtemp, + ) + await self.async_set_fan_mode(self._auto_deactivated_fan_mode) + + def choose_auto_regulation_mode(self, auto_regulation_mode: str): + """Choose or change the regulation mode""" + self._auto_regulation_mode = auto_regulation_mode + if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT: + self._regulation_algo = PITemperatureRegulator( + self.target_temperature, + RegulationParamLight.kp, + RegulationParamLight.ki, + RegulationParamLight.k_ext, + RegulationParamLight.offset_max, + RegulationParamLight.accumulated_error_threshold, + RegulationParamLight.overheat_protection, + ) + elif self._auto_regulation_mode == CONF_AUTO_REGULATION_MEDIUM: + self._regulation_algo = PITemperatureRegulator( + self.target_temperature, + RegulationParamMedium.kp, + RegulationParamMedium.ki, + RegulationParamMedium.k_ext, + RegulationParamMedium.offset_max, + RegulationParamMedium.accumulated_error_threshold, + RegulationParamMedium.overheat_protection, + ) + elif self._auto_regulation_mode == CONF_AUTO_REGULATION_STRONG: + self._regulation_algo = PITemperatureRegulator( + self.target_temperature, + RegulationParamStrong.kp, + RegulationParamStrong.ki, + RegulationParamStrong.k_ext, + RegulationParamStrong.offset_max, + RegulationParamStrong.accumulated_error_threshold, + RegulationParamStrong.overheat_protection, + ) + elif self._auto_regulation_mode == CONF_AUTO_REGULATION_SLOW: + self._regulation_algo = PITemperatureRegulator( + self.target_temperature, + RegulationParamSlow.kp, + RegulationParamSlow.ki, + RegulationParamSlow.k_ext, + RegulationParamSlow.offset_max, + RegulationParamSlow.accumulated_error_threshold, + RegulationParamSlow.overheat_protection, + ) + elif self._auto_regulation_mode == CONF_AUTO_REGULATION_EXPERT: + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api( + self._hass + ) + if api: + if (expert_param := api.self_regulation_expert) is not None: + self._regulation_algo = PITemperatureRegulator( + self.target_temperature, + expert_param.get("kp"), + expert_param.get("ki"), + expert_param.get("k_ext"), + expert_param.get("offset_max"), + expert_param.get("accumulated_error_threshold"), + expert_param.get("overheat_protection", True), + ) + else: + _LOGGER.error( + "%s - Cannot initialize Expert self-regulation mode due to VTherm API doesn't exists. Please contact the publisher of the integration", + self, + ) + else: + _LOGGER.error( + "%s - Cannot initialize Expert self-regulation mode cause the configuration in configuration.yaml have not been found. Please see readme documentation for %s", + self, + DOMAIN, + ) + + if not self._regulation_algo: + # A default empty algo (which does nothing) + self._regulation_algo = PITemperatureRegulator(self.target_temperature, 0, 0, 0, 0, 0, True) + + def choose_auto_fan_mode(self, auto_fan_mode: str): + """Choose the correct fan mode depending of the underlying capacities and the configuration""" + + self._current_auto_fan_mode = auto_fan_mode + + # Get the supported feature of the first underlying. We suppose each underlying have the same fan attributes + fan_supported = (self.supported_features or 0) & ClimateEntityFeature.FAN_MODE > 0 + + if auto_fan_mode == CONF_AUTO_FAN_NONE or not fan_supported: + self._auto_activated_fan_mode = self._auto_deactivated_fan_mode = None + return + + def find_fan_mode(fan_modes: list[str], fan_mode: str) -> str | None: + """Return the fan_mode if it exist of None if not""" + try: + return fan_mode if fan_modes.index(fan_mode) >= 0 else None + except ValueError: + return None + + def determine_fan_mode_contains_speed(fan_modes: list[str]) -> bool: + """Determine if the fan_modes contains speed modes by searching for the keywords "low"/"1".""" + for val in ["low", "1"]: + if find_fan_mode(fan_modes, val): + return True + return False + + def fix_order_speed_modes(speed_modes: list) -> list: + """Determine if speed_modes list is ordered from high to low speed and reverse it""" + index = -1 + if "low" in speed_modes: + index = speed_modes.index("low") + elif "1" in speed_modes: + index = speed_modes.index("1") + + if index > -1 and index >= len(speed_modes) / 2: + speed_modes.reverse() + + return speed_modes + + # Remove special modes like "auto" + fan_modes = self.fan_modes or [] + speed_modes = [ + mode for mode in fan_modes + if mode not in ["auto"] + ] + + num_speeds = len(speed_modes) + if num_speeds == 0: + self._auto_activated_fan_mode = None + return + + # We suppose speed_modes are ordered from low to high speed + speed_modes = fix_order_speed_modes(speed_modes) + + # We suppose that the speed modes contains at least 3 values + # fan_modes = low, medium, high : + # |CONF_AUTO_FAN_LOW |low | + # |CONF_AUTO_FAN_MEDIUM |medium | + # |CONF_AUTO_FAN_HIGH |high | + # |CONF_AUTO_FAN_TURBO |high | + # fan_modes = low, medium, high, turbo : + # |CONF_AUTO_FAN_LOW |low | + # |CONF_AUTO_FAN_MEDIUM |medium | + # |CONF_AUTO_FAN_HIGH |high | + # |CONF_AUTO_FAN_TURBO |turbo | + # fan_modes = low, medium_low, medium, medium_high, high : + # |CONF_AUTO_FAN_LOW |medium_low | + # |CONF_AUTO_FAN_MEDIUM |medium | + # |CONF_AUTO_FAN_HIGH |medium_high | + # |CONF_AUTO_FAN_TURBO |high | + target_index = -1 + + if determine_fan_mode_contains_speed(fan_modes) is False: + self._auto_activated_fan_mode = None + _LOGGER.warning( + "%s - #1419 - choose_auto_fan_mode cannot define value because fan_modes=%s doesn't contains speed values", + self, + self.fan_modes, + ) + + return + + if auto_fan_mode == CONF_AUTO_FAN_LOW: + if num_speeds >= 4: + target_index = num_speeds - 4 + else: + target_index = 0 + elif auto_fan_mode == CONF_AUTO_FAN_MEDIUM: + if num_speeds >= 4: + target_index = num_speeds - 3 + else: + target_index = 1 + elif auto_fan_mode == CONF_AUTO_FAN_HIGH: + if num_speeds >= 4: + target_index = num_speeds - 2 + else: + target_index = 2 + elif auto_fan_mode == CONF_AUTO_FAN_TURBO: + target_index = num_speeds - 1 + + if target_index >= 0: + self._auto_activated_fan_mode = speed_modes[target_index] + else: + self._auto_activated_fan_mode = None + + for val in AUTO_FAN_DEACTIVATED_MODES: + if find_fan_mode(fan_modes, val): + self._auto_deactivated_fan_mode = val + break + + _LOGGER.info( + "%s - choose_auto_fan_mode founds current_auto_fan_mode=%s auto_activated_fan_mode=%s and auto_deactivated_fan_mode=%s", + self, + self._current_auto_fan_mode, + self._auto_activated_fan_mode, + self._auto_deactivated_fan_mode, + ) + + @overrides + async def async_added_to_hass(self): + """Run when entity about to be added.""" + await super().async_added_to_hass() + + # Add listener to all underlying entities + # TODO should be triggered by Underlying class ? -> yes + # for climate in self._underlyings: + # self.async_on_remove( + # async_track_state_change_event( + # self.hass, [climate.entity_id], self._async_climate_changed + # ) + # ) + + # Start the control_heating + # starts a cycle + self.async_on_remove( + async_track_time_interval( + self.hass, + self.async_control_heating, + interval=timedelta(minutes=self._cycle_min), + ) + ) + + # Synchronize temperature if activated + await self.synchronize_device_temperature() + + @overrides + def restore_specific_previous_state(self, old_state: State): + """Restore my specific attributes from previous state""" + old_error = old_state.attributes.get("regulation_accumulated_error") + if old_error: + self._regulation_algo.set_accumulated_error(old_error) + _LOGGER.debug( + "%s - Old regulation accumulated_error have been restored to %f", + self, + old_error, + ) + + @overrides + def update_custom_attributes(self): + """Custom attributes""" + super().update_custom_attributes() + + self._attr_extra_state_attributes["fan_mode"] = self.fan_mode + self._attr_extra_state_attributes["fan_modes"] = self.fan_modes + self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate + # the attr is 2 times in custom_attributes, because it need to be restored, so it must be at root + self._attr_extra_state_attributes["regulation_accumulated_error"] = self._regulation_algo.accumulated_error + self._attr_extra_state_attributes["regulated_target_temperature"] = self.regulated_target_temp + vtherm_over_climate_data = { + "start_hvac_action_date": self._underlying_climate_start_hvac_action_date, + "last_mean_power_cycle": self._underlying_climate_mean_power_cycle, + "underlying_entities": [underlying.entity_id for underlying in self._underlyings], + "is_regulated": self.is_regulated, + "auto_fan_mode": self.auto_fan_mode, + "current_auto_fan_mode": self._current_auto_fan_mode, + "auto_activated_fan_mode": self._auto_activated_fan_mode, + "auto_deactivated_fan_mode": self._auto_deactivated_fan_mode, + "follow_underlying_temp_change": self._follow_underlying_temp_change, + "auto_regulation_use_device_temp": self.auto_regulation_use_device_temp, + } + + if self.is_regulated: + vtherm_over_climate_data["regulation"] = { + "regulated_target_temperature": self._regulated_target_temp, + "auto_regulation_mode": self._auto_regulation_mode, + "regulation_accumulated_error": self._regulation_algo.accumulated_error, + } + + if self.has_sync_entities: + under_attributes = {} + for idx, under in enumerate(self._underlyings): + try: + state = self.hass.states.get(self._sync_entity_list[idx]) + value = float(state.state) if state is not None else None + except (ValueError, AttributeError, TypeError): + value = None + + under_attributes[self._sync_entity_list[idx]] = { + "value": value, + "min_sync_entity": under.min_sync_entity, + "max_sync_entity": under.max_sync_entity, + "step_sync_entity": under.step_sync_entity, + } + vtherm_over_climate_data["temp_synchronisation"] = { + "sync_entity_ids": self._sync_entity_list, + "sync_with_calibration": self._sync_with_calibration, + "sync_attributes": under_attributes, + } + + self._attr_extra_state_attributes.update({"vtherm_over_climate": vtherm_over_climate_data}) + + # _LOGGER.debug("%s - Calling update_custom_attributes: %s", self, self._attr_extra_state_attributes) + + @overrides + def recalculate(self, force=False): + """A utility function to force the calculation of a the algo. For over_climate there is nothing to + recalculate but we need it cause the base function throw not implemented error + """ + + @overrides + def incremente_energy(self): + """increment the energy counter if device is active""" + + # if self.vtherm_hvac_mode == VThermHvacMode_OFF: + # return + + device_power = self._underlying_climate_mean_power_cycle + + added_energy = 0 + self._underlying_climate_delta_t = 0 + if self._underlying_climate_start_hvac_action_date: + delta = self.now - self._underlying_climate_start_hvac_action_date + self._underlying_climate_delta_t = delta.total_seconds() / 3600.0 + + if self.is_over_climate and self._underlying_climate_delta_t > 0 and device_power is not None and device_power > 0: + added_energy = device_power * self._underlying_climate_delta_t + + 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, + ) + + self._underlying_climate_start_hvac_action_date = self.now if self.should_device_be_active else None + + _LOGGER.debug( + "%s - added energy is %.3f . Total energy is now: %.3f", + self, + added_energy, + self._total_energy, + ) + + @callback + async def underlying_changed( # pylint: disable=too-many-arguments + self, + under: UnderlyingClimate, + new_hvac_mode: VThermHvacMode | None, + new_hvac_action: HVACAction | None, + new_target_temp: float | None, + new_fan_mode: str | None, + new_state: State, + old_state: State, + ): + """Handle underlying changes. This is called by the UnderlyingClimate class when the entity changes. The UnderlyingClimate does some checks to ensure that the change is relevant. When an attribute is not changed, the corresponding parameter is None.""" + + write_event_log(_LOGGER, self, f"Underlying climate {under.entity_id}state changed from {old_state} to new_state {new_state}") + + async def end_climate_changed(changes: bool): + """To end the event management""" + if changes: + self.requested_state.force_changed() + await self.update_states() + + changes = False + + # A real changes have to be managed + _LOGGER.debug( + "%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)", + self, + under.entity_id, + new_hvac_mode if new_hvac_mode is not None else "", + self.vtherm_hvac_mode, + new_hvac_action if new_hvac_action is not None else "", + self.hvac_action, + new_target_temp if new_target_temp is not None else "", + self.target_temperature, + new_fan_mode if new_fan_mode is not None else "", + self.fan_mode, + ) + + # Check that the state is defined + if new_state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]: + _LOGGER.info( + "%s - Underlying climate %s is in state %s. We consider that there is no change to do", + self, + under.entity_id, + new_state.state, + ) + await end_climate_changed(changes) + # TODO add a specific attribute to know that the underlying is unavailable and manage it in the error messages + return + + # Interpretation of hvac action + if new_hvac_action: + old_hvac_action = old_state.attributes.get("hvac_action") if old_state and old_state.attributes else None + if old_hvac_action not in HVAC_ACTION_ON and new_hvac_action in HVAC_ACTION_ON: + self._underlying_climate_start_hvac_action_date = self.now # self.get_last_updated_date_or_now(new_state) the event has the system date + self._underlying_climate_mean_power_cycle = self.power_manager.mean_cycle_power + _LOGGER.info( + "%s - underlying just switch ON. Set power and energy start date %s", + self, + self._underlying_climate_start_hvac_action_date.isoformat(), + ) + changes = True + + if old_hvac_action in HVAC_ACTION_ON and new_hvac_action not in HVAC_ACTION_ON: + self.incremente_energy() + changes = True + + # Filter new state when received just after a change from VTherm + # Issue #120 - Some TRV are changing target temperature a very long time (6 sec) after the change. + # In that case a loop is possible if a user change multiple times during this 6 sec. + new_state_date_updated = new_state.last_updated if new_state.last_updated else None + if new_state_date_updated and self._last_change_time_from_vtherm: + delta = (new_state_date_updated - self._last_change_time_from_vtherm).total_seconds() + if delta < 10: + _LOGGER.info( + "%s - underlying event is received less than 10 sec after command. Forget it to avoid loop", + self, + ) + await end_climate_changed(changes) + return + + # Update all underlyings hvac_mode state if it has change + if new_hvac_mode: + # Issue #334 - if all underlyings are not aligned with the same hvac_mode don't change the underlying and wait they are aligned + if self.is_over_climate: + for under in self._underlyings: + if under.entity_id != new_state.entity_id and under.hvac_mode != self.vtherm_hvac_mode: + _LOGGER.info( + "%s - the underlying's hvac_mode %s is not aligned with VTherm hvac_mode %s. So we don't diffuse the change to all other underlyings to avoid loops", + under, + under.hvac_mode, + self.vtherm_hvac_mode, + ) + return + + _LOGGER.debug( + "%s - All underlyings have the same hvac_mode '%s'", + self, + new_hvac_mode, + ) + changes = True + # We follow the underlying hvac_mode change + if self._follow_underlying_temp_change: + _LOGGER.debug( + "%s - Follow is 'on'. Changing hvac_mode for all underlying to '%s'", + self, + new_hvac_mode, + ) + self.requested_state.set_hvac_mode(new_hvac_mode) + + # A quick win to known if it has change by using the self._attr_fan_mode and not only underlying[0].fan_mode + if new_fan_mode: + self._attr_fan_mode = new_fan_mode + changes = True + + # Manage new target temperature set if state if no other changes have been found + # and if a target temperature have already been sent and if the VTherm is on + if new_target_temp and self._follow_underlying_temp_change and not changes: + if under.last_sent_temperature is not None and self.vtherm_hvac_mode != VThermHvacMode_OFF: + _LOGGER.debug( + "%s - Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s", + self, + under.last_sent_temperature, + new_target_temp, + ) + _LOGGER.info( + "%s - Target temp in underlying have change to %s (vs %s)", + self, + new_target_temp, + under.last_sent_temperature, + ) + await self.async_set_temperature(temperature=new_target_temp) + changes = True + + await end_climate_changed(changes) + + @overrides + async def async_control_heating(self, timestamp=None, force=False) -> bool: + """The main function used to run the calculation at each cycle""" + + if not self.is_ready: + _LOGGER.debug("%s - async_control_heating is called but the entity is not initialized yet. Skip the cycle", self) + return False + + # Check if we need to auto start/stop the Vtherm + old_stop = self.auto_start_stop_manager.is_auto_stop_detected + new_stop = await self.auto_start_stop_manager.refresh_state() + if old_stop != new_stop: + _LOGGER.info("%s - Auto stop state changed from %s to %s", self, old_stop, new_stop) + self.requested_state.force_changed() + await self.update_states(force=True) + return True + + # Continue the normal async_control_heating + + # Send the regulated temperature to the underlyings + await self._send_regulated_temperature(force=force) + + if self._auto_fan_mode and self._auto_fan_mode != CONF_AUTO_FAN_NONE: + await self._send_auto_fan_mode() + + ret = await super().async_control_heating(timestamp=timestamp, force=force) + + self.incremente_energy() + + return ret + + 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() + self.async_write_ha_state() + + @overrides + async def _async_temperature_changed(self, event: Event) -> callable: + """Handle temperature of the temperature sensor changes. + Return the function to dearm (clear) the window auto check""" + + ret = await super()._async_temperature_changed(event) + + # Synchronize the device temperature if needed + await self.synchronize_device_temperature() + + return ret + + async def synchronize_device_temperature(self): + """Synchronize the device temperature by sending the offset calibration""" + + if not self.has_sync_entities: + return + + for idx, sync_entity_id in enumerate(self._sync_entity_list): + sync_entity_state = self._hass.states.get(sync_entity_id) + if not sync_entity_state: + _LOGGER.warning( + "%s - Cannot synchronize device temperature because sync entity %s not found", + self, + sync_entity_id, + ) + continue + + under = self.underlying_entity(idx) + if not under: + _LOGGER.warning( + "%s - Cannot synchronize device temperature because underlying index %d not found", + self, + idx, + ) + continue + + if ( + (min_sync_entity := under.min_sync_entity) is not None + and (max_sync_entity := under.max_sync_entity) is not None + and (step_sync_entity := under.step_sync_entity) is not None + ): + pass + else: + # get min, max, step from sync entity attributes + min_sync_entity = sync_entity_state.attributes.get("min") + max_sync_entity = sync_entity_state.attributes.get("max") + step_sync_entity = sync_entity_state.attributes.get("step") or 0.1 # default step is 0.1 + + # save the min, max and step + under.set_min_max_step_sync_entity( + min_sync_entity, + max_sync_entity, + step_sync_entity, + ) + + room_temp = self.current_temperature + if self._sync_with_calibration: + # send offset_calibration to the difference between target temp and local temp + offset = None + local_temp = under.underlying_current_temperature + current_offset = get_safe_float(self._hass, sync_entity_id) + if local_temp is not None and room_temp is not None and current_offset is not None: + val = round_to_nearest(room_temp - (local_temp - current_offset), step_sync_entity) + offset = min(max_sync_entity, max(min_sync_entity, val)) + + _LOGGER.debug( + "%s - Synchronize device temperature for entity %s: local_temp=%.2f, room_temp=%.2f, current_offset=%.2f -> new offset=%.2f", + self, + sync_entity_id, + local_temp, + room_temp, + current_offset, + offset, + ) + if offset != current_offset: + await under.send_value_to_number(sync_entity_id, offset) + elif room_temp is not None: + # Send the new temperature directly + val = round_to_nearest(room_temp, step_sync_entity) + val = min(max_sync_entity, max(min_sync_entity, val)) + _LOGGER.debug( + "%s - Synchronize device temperature for entity %s: room_temp=%.2f -> new temp=%.2f", + self, + sync_entity_id, + room_temp, + val, + ) + await under.send_value_to_number(sync_entity_id, val) + + @property + def has_sync_entities(self) -> bool: + """Return True if the underlying have a sync entity""" + return self._sync_entity_list is not None and len(self._sync_entity_list) > 0 + + @property + def is_sync_with_calibration(self) -> bool: + """Return True if the underlying is synchronized with calibration (or with temperature copying)""" + return self._sync_with_calibration + + @property + def sync_entity_ids(self) -> list[str] | None: + """Get the sync entity ids""" + return self._sync_entity_list + + @property + def auto_regulation_mode(self) -> str | None: + """Get the regulation mode""" + return self._auto_regulation_mode + + @property + def auto_fan_mode(self) -> str | None: + """Get the auto fan mode""" + return self._auto_fan_mode + + @property + def auto_regulation_use_device_temp(self) -> bool | None: + """Returns the value of parameter auto_regulation_use_device_temp""" + return self._auto_regulation_use_device_temp + + @property + def regulated_target_temp(self) -> float | None: + """Get the regulated target temperature""" + return self._regulated_target_temp + + @property + def is_regulated(self) -> bool: + """Check if the ThermostatOverClimate is regulated""" + return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE + + @overrides + def build_hvac_list(self) -> list[VThermHvacMode]: + """Build the hvac list depending on ac_mode""" + if self.underlying_entity(0): + # replace HEAT_COOL by heat and cool + result = under_hvac_modes = self.underlying_entity(0).hvac_modes + if VThermHvacMode_HEAT_COOL in under_hvac_modes: + result = [mode for mode in under_hvac_modes if mode != VThermHvacMode_HEAT_COOL] + if VThermHvacMode_HEAT not in under_hvac_modes: + result.extend([VThermHvacMode_HEAT]) + if VThermHvacMode_COOL not in under_hvac_modes: + result.extend([VThermHvacMode_COOL]) + + return result + + if self._ac_mode: + return [VThermHvacMode_HEAT, VThermHvacMode_COOL, VThermHvacMode_OFF] + + return [VThermHvacMode_HEAT, VThermHvacMode_OFF] + + @property + def fan_mode(self) -> str | None: + """Return the fan setting. + + Requires ClimateEntityFeature.FAN_MODE. + """ + if self.underlying_entity(0): + self._attr_fan_mode = self.underlying_entity(0).fan_mode + return self._attr_fan_mode + + return None + + @property + def fan_modes(self) -> list[str] | None: + """Return the list of available fan modes. + + Requires ClimateEntityFeature.FAN_MODE. + """ + if self.underlying_entity(0): + return self.underlying_entity(0).fan_modes + + return [] + + @property + def swing_mode(self) -> str | None: + """Return the swing setting. + + Requires ClimateEntityFeature.SWING_MODE. + """ + if self.underlying_entity(0): + return self.underlying_entity(0).swing_mode + + return None + + @property + def swing_modes(self) -> list[str] | None: + """Return the list of available swing modes. + + Requires ClimateEntityFeature.SWING_MODE. + """ + if self.underlying_entity(0): + return self.underlying_entity(0).swing_modes + + return None + + @property + def swing_horizontal_mode(self) -> str | None: + """Return the swing horizontal setting. + + Requires ClimateEntityFeature.SWING_HORIZONTAL_MODE. + """ + if self.underlying_entity(0): + return self.underlying_entity(0).swing_horizontal_mode + + return None + + @property + def swing_horizontal_modes(self) -> list[str] | None: + """Return the list of available swing horizontal modes. + + Requires ClimateEntityFeature.SWING_HORIZONTAL_MODE. + """ + if self.underlying_entity(0): + return self.underlying_entity(0).swing_horizontal_modes + + return None + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return self.hass.config.units.temperature_unit + + @property + def supported_features(self): + """Return the list of supported features.""" + if self.underlying_entity(0) and self.underlying_entity(0).supported_features: + return self.underlying_entity(0).supported_features | self._support_flags + + return self._support_flags + + @property + def target_temperature_high(self) -> float | None: + """Return the highbound target temperature we try to reach. + + Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE. + """ + if self.underlying_entity(0): + return self.underlying_entity(0).target_temperature_high + + return None + + @property + def target_temperature_low(self) -> float | None: + """Return the lowbound target temperature we try to reach. + + Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE. + """ + if self.underlying_entity(0): + return self.underlying_entity(0).target_temperature_low + + 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. + + Requires ClimateEntityFeature.AUX_HEAT. + """ + if self.underlying_entity(0): + return self.underlying_entity(0).is_aux_heat + + return None + + @property + def follow_underlying_temp_change(self) -> bool: + """Get the follow underlying temp change flag""" + return self._follow_underlying_temp_change + + @overrides + async def init_underlyings_completed(self, under_entity_id: Optional[str] = None): + """Called when all entities of an underlying are initialized. Then we can complete the startup with the underlying info""" + if not self.is_ready: + return + + # Reinitialize the hvac list because we have one underlying at least now + self.set_hvac_list() + await super().init_underlyings_completed(under_entity_id) + + self.choose_auto_fan_mode(self._auto_fan_mode) + + @overrides + def turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + if self.underlying_entity(0): + return self.underlying_entity(0).turn_aux_heat_on() + + raise NotImplementedError() + + @overrides + async def async_turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + for under in self._underlyings: + await under.async_turn_aux_heat_on() + + @overrides + def turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + for under in self._underlyings: + return under.turn_aux_heat_off() + + @overrides + async def async_turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + for under in self._underlyings: + await under.async_turn_aux_heat_off() + + @overrides + async def async_set_fan_mode(self, fan_mode: str): + """Set new target fan mode.""" + _LOGGER.info("%s - Set fan mode: %s", self, fan_mode) + if fan_mode is None: + return + + for under in self._underlyings: + await under.set_fan_mode(fan_mode) + self._fan_mode = fan_mode + self.async_write_ha_state() + + @overrides + async def async_set_humidity(self, humidity: int): + """Set new target humidity.""" + _LOGGER.info("%s - Set humidity: %s", self, humidity) + if humidity is None: + return + for under in self._underlyings: + await under.set_humidity(humidity) + self._humidity = humidity + self.async_write_ha_state() + + @overrides + async def async_set_swing_mode(self, swing_mode): + """Set new target swing operation.""" + _LOGGER.info("%s - Set swing mode: %s", self, swing_mode) + if swing_mode is None: + return + for under in self._underlyings: + await under.set_swing_mode(swing_mode) + self._swing_mode = swing_mode + self.async_write_ha_state() + + @overrides + async def async_set_swing_horizontal_mode(self, swing_horizontal_mode): + """Set new target swing horizontal operation.""" + _LOGGER.info("%s - Set swing horizontal mode: %s", self, swing_horizontal_mode) + if swing_horizontal_mode is None: + return + for under in self._underlyings: + await under.set_swing_horizontal_mode(swing_horizontal_mode) + self._swing_horizontal_mode = swing_horizontal_mode + self.async_write_ha_state() + + async def service_set_auto_regulation_mode(self, auto_regulation_mode: str): + """Called by a service call: + service: versatile_thermostat.set_auto_regulation_mode + data: + auto_regulation_mode: [None | Light | Medium | Strong] + target: + entity_id: climate.thermostat_1 + """ + _LOGGER.info( + "%s - Calling service_set_auto_regulation_mode, auto_regulation_mode: %s", + self, + auto_regulation_mode, + ) + if auto_regulation_mode == "None": + self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_NONE) + elif auto_regulation_mode == "Light": + self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_LIGHT) + elif auto_regulation_mode == "Medium": + self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_MEDIUM) + elif auto_regulation_mode == "Strong": + self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_STRONG) + elif auto_regulation_mode == "Slow": + 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() + self.async_write_ha_state() + + async def service_set_auto_fan_mode(self, auto_fan_mode: str): + """Called by a service call: + service: versatile_thermostat.set_auto_fan_mode + data: + auto_fan_mode: [None | Low | Medium | High | Turbo] + target: + entity_id: climate.thermostat_1 + """ + _LOGGER.info( + "%s - Calling service_set_auto_fan_mode, auto_fan_mode: %s", + self, + auto_fan_mode, + ) + if auto_fan_mode == "None": + self.choose_auto_fan_mode(CONF_AUTO_FAN_NONE) + elif auto_fan_mode == "Low": + self.choose_auto_fan_mode(CONF_AUTO_FAN_LOW) + elif auto_fan_mode == "Medium": + self.choose_auto_fan_mode(CONF_AUTO_FAN_MEDIUM) + elif auto_fan_mode == "High": + self.choose_auto_fan_mode(CONF_AUTO_FAN_HIGH) + elif auto_fan_mode == "Turbo": + self.choose_auto_fan_mode(CONF_AUTO_FAN_TURBO) + + self.update_custom_attributes() + self.async_write_ha_state() + + @overrides + async def async_turn_off(self) -> None: + """Turn off the climate entity.""" + self._last_hvac_mode = self.requested_state.hvac_mode + await self.async_set_hvac_mode(VThermHvacMode_OFF) + + @overrides + async def async_turn_on(self) -> None: + """Turn on the climate entity. If multiple modes are available, choose the most appropriate one.""" + if self._last_hvac_mode: + await self.async_set_hvac_mode(self._last_hvac_mode) + elif self._ac_mode: + await self.async_set_hvac_mode(VThermHvacMode_COOL) + else: + await self.async_set_hvac_mode(VThermHvacMode_HEAT) + + @property + def vtherm_type(self) -> str | None: + """Return the type of thermostat""" + return "over_climate" diff --git a/custom_components/versatile_thermostat/thermostat_climate_valve.py b/custom_components/versatile_thermostat/thermostat_climate_valve.py new file mode 100644 index 0000000..b7cc869 --- /dev/null +++ b/custom_components/versatile_thermostat/thermostat_climate_valve.py @@ -0,0 +1,638 @@ +# pylint: disable=line-too-long, too-many-lines, abstract-method +""" A climate with a direct valve regulation class """ + +import logging +from vtherm_api.log_collector import get_vtherm_logger +import asyncio +from datetime import datetime +from typing import Optional + +from homeassistant.core import Event, HomeAssistant, State +from homeassistant.components.climate import HVACAction, HVACMode +from homeassistant.helpers.event import EventStateChangedData, async_call_later +from homeassistant.exceptions import ServiceValidationError + +from .underlyings import UnderlyingValveRegulation, UnderlyingClimate + +from .base_thermostat import ConfigData +from .thermostat_climate import ThermostatOverClimate +from .thermostat_prop import ThermostatProp +from .cycle_scheduler import CycleScheduler + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons import write_event_log +from .vtherm_hvac_mode import VThermHvacMode, VThermHvacMode_OFF, VThermHvacMode_SLEEP + +# from .vtherm_central_api import VersatileThermostatAPI + +_LOGGER = get_vtherm_logger(__name__) + + +class ThermostatOverClimateValve(ThermostatProp[UnderlyingClimate], 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", + "vtherm_over_climate", + "vtherm_over_climate_valve", + } + ) + ) + + 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 + self._min_opening_degress: list[int] = [] + self._max_closing_degree: int = 100 + self._opening_threshold_degree: int = 0 + self._recalibrate_lock: asyncio.Lock = asyncio.Lock() + self._climate_under_initialized: bool = False + self._valve_under_initialized: bool = False + + 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 + ) + + opening_list = config_entry.get(CONF_OPENING_DEGREE_LIST) + closing_list = config_entry.get(CONF_CLOSING_DEGREE_LIST, []) + self._max_closing_degree = config_entry.get(CONF_MAX_CLOSING_DEGREE, 100) + self._opening_threshold_degree = config_entry.get(CONF_OPENING_THRESHOLD_DEGREE, 0) + regulation_threshold = config_entry.get(CONF_AUTO_REGULATION_DTEMP, 0) + + self._min_opening_degrees = config_entry.get(CONF_MIN_OPENING_DEGREES, None) + min_opening_degrees_list = [] + if self._min_opening_degrees: + min_opening_degrees_list = [ + int(x.strip()) for x in self._min_opening_degrees.split(",") + ] + + self._max_opening_degrees = config_entry.get(CONF_MAX_OPENING_DEGREES, None) + max_opening_degrees_list = [] + if self._max_opening_degrees: + max_opening_degrees_list = [ + int(x.strip()) for x in self._max_opening_degrees.split(",") + ] + + for idx, _ in enumerate(config_entry.get(CONF_UNDERLYING_LIST)): + # number of opening should equal number of underlying + opening = opening_list[idx] + closing = closing_list[idx] if idx < len(closing_list) else None + self._opening_threshold_degree = max(self._opening_threshold_degree, regulation_threshold) + # TODO c'est pas possible ici + opening_entity = self._hass.states.get(opening) + + under = UnderlyingValveRegulation( + hass=self._hass, + thermostat=self, + opening_degree_entity_id=opening, + closing_degree_entity_id=closing, + climate_underlying=self._underlyings[idx], + min_opening_degree=(min_opening_degrees_list[idx] if idx < len(min_opening_degrees_list) else 0), + # TODO pas bon + max_opening_degree=(max_opening_degrees_list[idx] if idx < len(max_opening_degrees_list) else opening_entity.attributes.get("max", 100) if opening_entity else 100), + max_closing_degree=self._max_closing_degree, + opening_threshold=self._opening_threshold_degree, + ) + self._underlyings_valve_regulation.append(under) + + self._bind_scheduler(CycleScheduler( + hass=self._hass, + thermostat=self, + underlyings=self._underlyings_valve_regulation, + cycle_duration_sec=self._cycle_min * 60, + min_activation_delay=self.minimal_activation_delay, + min_deactivation_delay=self.minimal_deactivation_delay, + )) + + async def init_underlyings_completed(self, under_entity_id: Optional[str] = None): + """Called when an underlying is fully initialized + Caution: this method is called for the _underlyings of the ThermostatClimate but also for the underlyings_valve_regulation + We have to call the parent method only when the both underlyings are initialized""" + + _LOGGER.debug("%s - init_underlyings_completed called for %s", self, under_entity_id) + if not self.is_ready: + return + + _LOGGER.debug("%s - both climate and valve underlyings are initialized", self) + + await super().init_underlyings_completed(under_entity_id) + + async def async_startup(self, central_configuration): + """Startup the Entity. Listen to the underlying state changes""" + await super().async_startup(central_configuration) + + # Register the valve listener + for under in self._underlyings_valve_regulation: + _LOGGER.debug("%s - starting underlying valve regulation %s", self, under) + try: + under.startup() + _LOGGER.debug("%s - underlying valve regulation %s started successfully", self, under) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error("%s - Error starting underlying valve regulation %s: %s", self, under, ex) + + @overrides + def restore_specific_previous_state(self, old_state: State): + """Restore my specific attributes from previous state""" + super().restore_specific_previous_state(old_state) + + if self.is_sleeping: + self.set_hvac_off_reason(HVAC_OFF_REASON_SLEEP_MODE) + + @property + def is_initialized(self) -> bool: + """Check if all underlyings and valve underlyings are initialized""" + if not super().is_initialized: + return False + + for under in self._underlyings_valve_regulation: + if not under.is_initialized: + return False + return True + + @overrides + def update_custom_attributes(self): + """Custom attributes""" + super().update_custom_attributes() + + valve_attributes = {} + for under in self._underlyings_valve_regulation: + valve_attributes.update( + { + under.entity_id: { + "hvac_action": under.hvac_action, + "percent_open": under.percent_open, + "last_sent_opening_value": under.last_sent_opening_value, + "min_opening_degree": under._min_opening_degree, # pylint: disable=protected-access + "max_opening_degree": under._max_opening_degree, # pylint: disable=protected-access + } + } + ) + + self._attr_extra_state_attributes["valve_open_percent"] = self.valve_open_percent + self._attr_extra_state_attributes["power_percent"] = self.power_percent + self._attr_extra_state_attributes["on_percent"] = self.safe_on_percent + self._attr_extra_state_attributes.update( + { + "vtherm_over_climate_valve": { + "have_valve_regulation": self.have_valve_regulation, + "valve_regulation": { + "underlyings_valve_regulation": [underlying.valve_entity_ids for underlying in self._underlyings_valve_regulation], + "on_percent": self.safe_on_percent, + "power_percent": self.power_percent, + "function": self._proportional_function, + "tpi_coef_int": self._tpi_coef_int, + "tpi_coef_ext": self._tpi_coef_ext, + "tpi_threshold_low": self._tpi_threshold_low, + "tpi_threshold_high": self._tpi_threshold_high, + "minimal_activation_delay": self._minimal_activation_delay, + "minimal_deactivation_delay": self._minimal_deactivation_delay, + "min_opening_degrees": self._min_opening_degrees, + "opening_threshold_degree": self._opening_threshold_degree, + "max_closing_degree": self._max_closing_degree, + "max_opening_degrees": self._max_opening_degrees, + "valve_open_percent": self.valve_open_percent, + "auto_regulation_dpercent": self._auto_regulation_dpercent, + "auto_regulation_period_min": self._auto_regulation_period_min, + "last_calculation_timestamp": (self._last_calculation_timestamp.astimezone(self._current_tz).isoformat() if self._last_calculation_timestamp else None), + }, + "underlying_valves": valve_attributes, + } + } + ) + + # self.async_write_ha_state() + # _LOGGER.debug("%s - Calling update_custom_attributes: %s", self, self._attr_extra_state_attributes) + + @overrides + def recalculate(self, force=False): + """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) + + self.stop_recalculate_later() + + # 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 not force and period < self._auto_regulation_period_min: + _LOGGER.info( + "%s - do not calculate TPI because regulation_period (%d) is not exceeded", + self, + period, + ) + self.do_recalculate_later() + return + + # Call parent TPI recalculate to perform the TPI algorithm calculation + super().recalculate(force) + + if self.is_sleeping: + new_valve_percent = 100 + else: + on_percent = self.safe_on_percent + if on_percent is None: + # Temperature not yet available; preserve the current valve position. + return + new_valve_percent = round(max(0, min(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 + + def do_recalculate_later(self): + """A utility function to set the valve open percent later on all underlyings""" + _LOGGER.debug("%s - do_recalculate_later call", self) + + async def callback_recalculate(_): + """Callback to set the valve percent""" + self.recalculate() + await self.async_control_heating(force=False) + self.update_custom_attributes() + self.async_write_ha_state() + + self.stop_recalculate_later() + + self._cancel_recalculate_later = async_call_later(self._hass, delay=20, action=callback_recalculate) + + async def _send_regulated_temperature(self, force=False): + """Sends the regulated temperature to all underlying""" + # if self.vtherm_hvac_mode == VThermHvacMode_OFF and not self._is_sleeping: + # _LOGGER.debug("%s - don't send regulated temperature cause VTherm is off ", self) + # return + + if self.target_temperature is None: + _LOGGER.warning( + "%s - don't send regulated temperature cause VTherm target_temp (%s) is None. This should be a temporary warning message.", + self, + self.target_temperature, + ) + return + + if not force and not self.check_auto_regulation_period_min(self.now): + return + + # Don't send temperature if hvac_mode is off + if self.vtherm_hvac_mode != VThermHvacMode_OFF: + 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, + ) + + self._last_regulation_change = self.now + self.reset_last_change_time_from_vtherm() + + _LOGGER.debug( + "%s - last_regulation_change is now: %s and last_change_from_vtherm is now: %s", self, self._last_regulation_change, self._last_change_time_from_vtherm + ) # pylint: disable=protected-access + + for under in self._underlyings_valve_regulation: + await under.set_valve_open_percent() + + @overrides + def build_hvac_list(self) -> list[VThermHvacMode]: + """Build the hvac list depending on ac_mode""" + if self._ac_mode: + return [VThermHvacMode_COOL, VThermHvacMode_SLEEP, VThermHvacMode_OFF] + else: + return [VThermHvacMode_HEAT, VThermHvacMode_SLEEP, VThermHvacMode_OFF] + + @overrides + def incremente_energy(self): + """increment the energy counter if device is active""" + if self._underlying_climate_start_hvac_action_date: + stop_power_date = self.now + delta = stop_power_date - self._underlying_climate_start_hvac_action_date + self._underlying_climate_delta_t = delta.total_seconds() / 3600.0 + _LOGGER.debug("%s - underlying_climate_delta_t: %.4f hours", self, self._underlying_climate_delta_t) + # increment energy at the end of the cycle + super().incremente_energy() + self._underlying_climate_start_hvac_action_date = self.now + self._underlying_climate_mean_power_cycle = self.power_manager.mean_cycle_power + else: + _LOGGER.debug("%s - no underlying_climate_start_hvac_action_date to calculate energy", self) + + @property + def have_valve_regulation(self) -> bool: + """True if the Thermostat is regulated by valve""" + return True + + @property + def valve_open_percent(self) -> int: + """Gives the percentage of valve needed""" + if (self.vtherm_hvac_mode == VThermHvacMode_OFF and not self.is_sleeping) or self._valve_open_percent is None: + return 0 + else: + return self._valve_open_percent + + def calculate_hvac_action(self, under_list: list = None) -> HVACAction | None: + """Returns the current hvac_action by checking all hvac_action of the _underlyings_valve_regulation""" + + if self.is_sleeping: + self._attr_hvac_action = HVACAction.OFF + else: + super().calculate_hvac_action(self._underlyings_valve_regulation) + + @property + def should_device_be_active(self) -> bool: + """A hack to overrides the state from underlyings""" + if self.is_sleeping: + return False + + for under in self._underlyings_valve_regulation: + if under.should_device_be_active: + return True + return False + + @property + def device_actives(self) -> int: + """Calculate the number of active devices""" + if self.is_sleeping: + return [] + + return [under.opening_degree_entity_id for under in self._underlyings_valve_regulation if under.is_device_active] + + @property + def is_device_active(self) -> bool: + """Returns true if one underlying is active""" + if ThermostatOverClimate.is_device_active.fget(self) is not True: + return False + for under in self._underlyings_valve_regulation: + if under.is_device_active: + return True + return False + + @property + def activable_underlying_entities(self) -> list | None: + """Returns the activable underlying entities for controling the central boiler""" + return self._underlyings_valve_regulation + + @overrides + @property + def all_underlying_entities(self) -> list | None: + """Returns all underlying entities for controling the central boiler""" + return self._underlyings + self._underlyings_valve_regulation + + @overrides + @property + def is_sleeping(self) -> bool: + """True if the thermostat is in sleep mode""" + return self.vtherm_hvac_mode == VThermHvacMode_SLEEP + + @overrides + async def service_set_auto_regulation_mode(self, auto_regulation_mode: str): + """This should not be possible in valve regulation mode""" + return + + @overrides + async def service_set_hvac_mode_sleep(self): + """Set the hvac_mode to SLEEP mode (valid only for over_climate with valve regulation): + service: versatile_thermostat.set_hvac_mode_sleep + target: + entity_id: climate.thermostat_1 + """ + write_event_log(_LOGGER, self, "Calling SERVICE_SET_HVAC_MODE_SLEEP") + await self.async_set_hvac_mode(hvac_mode=VThermHvacMode_SLEEP) + + @overrides + def choose_auto_fan_mode(self, auto_fan_mode: str): + """Force no auto_fan for climate with valve regulation""" + self._current_auto_fan_mode = CONF_AUTO_FAN_NONE + self._auto_activated_fan_mode = self._auto_deactivated_fan_mode = None + + @property + def vtherm_type(self) -> str | None: + """Return the type of thermostat""" + return "over_climate_valve" + + @overrides + async def async_set_hvac_mode(self, hvac_mode: VThermHvacMode, ignore_lock: bool = False): + """Disable HVAC mode change during recalibration""" + if not self._recalibrate_lock.locked() or ignore_lock: + await super().async_set_hvac_mode(hvac_mode) + + @overrides + async def underlying_changed( # pylint: disable=too-many-arguments + self, + under: UnderlyingClimate, + new_hvac_mode: VThermHvacMode | None, + new_hvac_action: HVACAction | None, + new_target_temp: float | None, + new_fan_mode: str | None, + new_state: State, + old_state: State, + ): + """Handle underlying climate changes only if not in recalibration""" + if not self._recalibrate_lock.locked(): + return await super().underlying_changed(under, new_hvac_mode, new_hvac_action, new_target_temp, new_fan_mode, new_state, old_state) + _LOGGER.info("%s - ignore underlying climate change because recalibration is in progress", self) + + @overrides + async def async_set_temperature(self, **kwargs): + """Disable temperature change during recalibration""" + if not self._recalibrate_lock.locked(): + return await super().async_set_temperature(**kwargs) + _LOGGER.info("%s - ignore temperature change because recalibration is in progress", self) + + @overrides + async def async_set_preset_mode(self, preset_mode: str): + """Disable preset mode change during recalibration""" + if not self._recalibrate_lock.locked(): + return await super().async_set_preset_mode(preset_mode) + _LOGGER.info("%s - ignore preset mode change because recalibration is in progress", self) + + async def service_recalibrate_valves(self, delay_seconds: int): + """Start recalibration of valve opening/closing degrees for each underlying valve in background. + + Steps: + 1) memorize requested state + 2) set VTherm hvac mode to OFF + 3) for each valve: open to 100% (opening_degree=100, closing_degree=0), wait, + close to fully (opening_degree=0, closing_degree=100), wait + 4) restore requested state + + During this operation opening_threshold/min/max are ignored by sending + direct commands to the underlying number entities. + """ + if self.lock_manager.check_is_locked("service_recalibrate_valves"): + return {"message": "thermostat locked"} + + write_event_log(_LOGGER, self, f"Calling SERVICE_RECALIBRATE_VALVES delay_seconds={delay_seconds}") + + # Validate underlyings synchronously before launching background task + if not self._underlyings_valve_regulation: + raise ServiceValidationError(f"{self} - No valve regulation underlyings available") + + # Build a short validation list and capture entity min/max per underlying + valves_config = [] + for under in self._underlyings_valve_regulation: + opening = under.opening_degree_entity_id + closing = under.closing_degree_entity_id + if not opening: + raise ServiceValidationError(f"{self} - Underlying {under} must have opening degree entities configured") + + opening_state = self._hass.states.get(opening) + # closing_state = self._hass.states.get(closing) + if opening_state is None: + raise ServiceValidationError(f"{self} - Opening entity {opening} not found for underlying {under}") + + opening_min = opening_state.attributes.get("min", 0) + opening_max = opening_state.attributes.get("max", 100) + # closing_min = closing_state.attributes.get("min", 0) + # closing_max = closing_state.attributes.get("max", 100) + + closing_min: int | None = None + closing_max: int | None = None + + if closing: + closing_state = self._hass.states.get(closing) + + if closing_state is None: + raise ServiceValidationError(f"{self} - Closing entity {closing} not found for underlying {under}") + + closing_min = closing_state.attributes.get("min", 0) + closing_max = closing_state.attributes.get("max", 100) + + valves_config.append( + { + "under": under, + "opening": opening, + "closing": closing, + "opening_min": opening_min, + "opening_max": opening_max, + "closing_min": closing_min if closing else None, + "closing_max": closing_max if closing else None, + } + ) + + # Memorize expected/requested state + expected_state = self.requested_state.to_dict() if self.requested_state is not None else None + + # If a recalibration is already running, return immediately and do not schedule + if self._recalibrate_lock.locked(): + _LOGGER.warning("Recalibration request refused: already running for %s", self.entity_id) + return {"message": "recalibrage en cours"} + + # Define the background coroutine + async def _recalibrate_task(): + async with self._recalibrate_lock: + try: + # Turn off vtherm + _LOGGER.info("%s - Recalibration - Stopping VTherm and turn on underlying climates and waiting for %s seconds", self, delay_seconds) + await self.async_set_hvac_mode(VThermHvacMode_OFF, ignore_lock=True) + await asyncio.sleep(5) + + # Turn on the underlying climate + for under in self._underlyings: + await under.set_hvac_mode(HVACMode.HEAT) + + await asyncio.sleep(delay_seconds) + + _LOGGER.info("%s - Recalibration - Full opening of the valves and waiting for %s seconds", self, delay_seconds) + for cfg in valves_config: + under = cfg["under"] + opening = cfg["opening"] + closing = cfg["closing"] + + open_val = cfg["opening_max"] + close_val = cfg["closing_min"] + + _LOGGER.info("%s - Forcing opening=%s to %s and closing=%s to %s", self, opening, open_val, closing, close_val) + await under.send_value_to_number(opening, open_val) + if closing: + await under.send_value_to_number(closing, close_val) + + await asyncio.sleep(delay_seconds) + + _LOGGER.info("%s - Recalibration - Full closing of the valves and waiting for %s seconds", self, delay_seconds) + for cfg in valves_config: + under = cfg["under"] + opening = cfg["opening"] + closing = cfg["closing"] + + open_val2 = cfg["opening_min"] + close_val2 = cfg["closing_max"] + + _LOGGER.info("%s - Forcing opening=%s to %s and closing=%s to %s", self, opening, open_val2, closing, close_val2) + await under.send_value_to_number(opening, open_val2) + if closing: + await under.send_value_to_number(closing, close_val2) + await asyncio.sleep(delay_seconds) + + except Exception as exc: # pylint: disable=broad-except + _LOGGER.error("%s - Error during recalibration: %s", self, exc) + + # Restore requested state + _LOGGER.info("%s - Recalibration - Restoring requested state", self) + if expected_state: + try: + self.requested_state.set_state( + hvac_mode=expected_state.get("hvac_mode"), + target_temperature=expected_state.get("target_temperature"), + preset=expected_state.get("preset"), + ) + self.requested_state.force_changed() + await self.update_states(force=True) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error("%s - Cannot restore requested state after recalibration: %s", self, ex) + + # Launch background task and return immediately + try: + self.hass.async_create_task(_recalibrate_task()) + except Exception: # pylint: disable=broad-except + # fallback + self._hass.create_task(_recalibrate_task()) + + return {"message": "calibrage en cours"} diff --git a/custom_components/versatile_thermostat/thermostat_prop.py b/custom_components/versatile_thermostat/thermostat_prop.py new file mode 100644 index 0000000..0450381 --- /dev/null +++ b/custom_components/versatile_thermostat/thermostat_prop.py @@ -0,0 +1,344 @@ +# pylint: disable=line-too-long, abstract-method +"""Base class for proportional thermostats (TPI, SmartPI).""" +import logging +from vtherm_api.log_collector import get_vtherm_logger +from typing import Generic + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + +from .base_thermostat import BaseThermostat, ConfigData +from .underlyings import T +from .vtherm_hvac_mode import VThermHvacMode_OFF +from .const import CONF_PROP_FUNCTION, PROPORTIONAL_FUNCTION_TPI + +_LOGGER = get_vtherm_logger(__name__) + + +class ThermostatProp(BaseThermostat[T], Generic[T]): + """Base class for proportional thermostats. + + This class provides the common infrastructure for proportional + control algorithms (TPI, SmartPI). Algorithm-specific logic is + delegated to a handler via composition. + + Note: TPI-specific attributes (_tpi_coef_int, _proportional_function, etc.) + are inherited from BaseThermostat and updated by the handler during init. + """ + + def __init__(self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData): + """Initialize the proportional thermostat.""" + # Handler for algorithm-specific logic (TPI or SmartPI) + self._algo_handler = None + self._on_time_sec: float | None = 0 + self._off_time_sec: float | None = 0 + self._safety_state: bool = False + self._safety_default_on_percent: float = 0.0 + + super().__init__(hass, unique_id, name, entry_infos) + + # ========================================================================= + # COMMON PROPERTIES + # ========================================================================= + + @property + def has_prop(self) -> bool: + """True if the Thermostat uses a proportional algorithm (TPI, SmartPI).""" + return True + + @property + def prop_algorithm(self): + """Get the proportional algorithm.""" + return self._prop_algorithm + + @prop_algorithm.setter + def prop_algorithm(self, value): + """Set the proportional algorithm.""" + self._prop_algorithm = value + + @property + def proportional_algorithm(self): + """Get the proportional algorithm (alias).""" + return self._prop_algorithm + + @property + def on_percent(self) -> float | None: + """Returns the percentage the heater must be ON + In safety mode this value is overridden with the _default_on_percent. + Returns None when the temperature sensor was not available at the last + calculation. Callers must treat None as "temperature unknown — keep + the current switch state unchanged". + """ + if self._safety_state: + val = self._safety_default_on_percent + elif self._prop_algorithm: + val = self._prop_algorithm.on_percent + if val is None: + # Temperature was not available at last calculation. + # Propagate None so callers can skip touching the switch. + return None + else: + val = 0 + + # Clamp with max_on_percent + # issue 538 - clamping with max_on_percent should be done here + if self._max_on_percent is not None and val > self._max_on_percent: + val = self._max_on_percent + + # Notify the algorithm of the realized power (if supported) + # Only if the value has been modified by safety or clamping + if self._prop_algorithm and hasattr(self._prop_algorithm, "update_realized_power"): + # Get what the algorithm proposes + algo_percent = self._prop_algorithm.on_percent + if algo_percent is not None and val != algo_percent: + self._prop_algorithm.update_realized_power(val) + + return val + + @property + def safe_on_percent(self) -> float: + """Return the on_percent safe value. + Deprecated: use on_percent directly as it now handles safety. + """ + return self.on_percent + + def set_safety(self, default_on_percent: float): + """Set a default value for on_percent (used for safety mode)""" + _LOGGER.info("%s - Set safety to ON with default_on_percent=%s", self, default_on_percent) + self._safety_state = True + self._safety_default_on_percent = default_on_percent + + def unset_safety(self): + """Unset the safety mode""" + _LOGGER.info("%s - Set safety to OFF", self) + self._safety_state = False + + @property + def auto_tpi_manager(self): + """Return the Auto TPI manager from handler.""" + return self._algo_handler.auto_tpi_manager if self._algo_handler else None + + @property + def on_time_sec(self) -> float | None: + """Return the on time in seconds""" + return self._on_time_sec + + @property + def off_time_sec(self) -> float | None: + """Return the off time in seconds""" + return self._off_time_sec + + # ========================================================================= + # LIFECYCLE METHODS - Delegate to handler + # ========================================================================= + + def post_init(self, config_entry: ConfigData): + """Finish the initialization of the thermostat.""" + super().post_init(config_entry) + + # Initialize off_time to full cycle duration (on_percent=0 at startup) + self._off_time_sec = int(self._cycle_min * 60) + + # Initialize the proportional function from config + # This allows selecting the correct handler (TPI, or other prop algorithms) + self._proportional_function = self._entry_infos.get(CONF_PROP_FUNCTION) + + # For external algorithms, don't raise if not registered yet — will retry at startup. + self._init_algorithm_handler( + raise_if_missing=(self._proportional_function == PROPORTIONAL_FUNCTION_TPI) + ) + + def _init_algorithm_handler(self, raise_if_missing: bool = True) -> bool: + """Initialize the algorithm handler based on proportional_function config. + + Returns True if the handler was successfully initialized, False if the external + algorithm was not yet registered (only possible when raise_if_missing=False). + """ + # Import here to avoid circular imports + from .prop_handler_tpi import TPIHandler # pylint: disable=import-outside-toplevel + from .vtherm_central_api import VersatileThermostatAPI # pylint: disable=import-outside-toplevel + + if self._proportional_function == PROPORTIONAL_FUNCTION_TPI: + self._algo_handler = TPIHandler(self) + self._algo_handler.init_algorithm() + return True + + api = VersatileThermostatAPI.get_vtherm_api(self.hass) + factory = ( + api.get_prop_algorithm(self._proportional_function) + if api is not None and hasattr(api, "get_prop_algorithm") + else None + ) + + if factory is not None: + self._algo_handler = factory.create(self) + self._algo_handler.init_algorithm() + return True + + if raise_if_missing: + raise ValueError( + f"{self} - Unknown proportional function: {self._proportional_function}" + ) + + _LOGGER.warning( + "%s - External proportional algorithm '%s' not yet registered. Will retry at startup.", + self, + self._proportional_function, + ) + return False + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + if self._algo_handler: + await self._algo_handler.async_added_to_hass() + await super().async_added_to_hass() + + async def async_startup(self, central_configuration): + """Startup the thermostat.""" + # External algorithm plugins register after VT entities are created. + # async_startup is called after EVENT_HOMEASSISTANT_STARTED so all plugins + # are guaranteed to be loaded at this point. + if self._algo_handler is None: + if not self._init_algorithm_handler(raise_if_missing=True): + return + if self._cycle_scheduler is not None: + self._algo_handler.on_scheduler_ready(self._cycle_scheduler) + # Catch up on the lifecycle call that was skipped at entity creation. + await self._algo_handler.async_added_to_hass() + + await super().async_startup(central_configuration) + if self._algo_handler: + await self._algo_handler.async_startup() + + def remove_thermostat(self): + """Called when the thermostat will be removed.""" + if self._algo_handler: + self._algo_handler.remove() + super().remove_thermostat() + + # ========================================================================= + # COMMON METHODS + # ========================================================================= + + def recalculate(self, force=False): + """Force the calculation of the algo and update attributes.""" + if self._prop_algorithm: + self._prop_algorithm.calculate( + self.target_temperature, + self._cur_temp, + self._cur_ext_temp, + self.last_temperature_slope, + self.vtherm_hvac_mode or VThermHvacMode_OFF, + power_shedding=self.is_overpowering_detected, + off_reason=self.hvac_off_reason, + ) + + async def _control_heating_specific(self, timestamp=None, force=False): + """Control heating using the algorithm handler.""" + if self._algo_handler: + await self._algo_handler.control_heating(timestamp, force) + + async def update_states(self, force=False): + """Update states and delegate to handler.""" + changed = await super().update_states(force) + if self._algo_handler: + # External proportional plugins may need to react to temperature + # crossings even when VT logical state did not change. + await self._algo_handler.on_state_changed(changed) + return changed + + def update_custom_attributes(self): + """Update custom attributes.""" + super().update_custom_attributes() + if self._algo_handler and hasattr(self._algo_handler, "update_attributes"): + self._algo_handler.update_attributes() + + # ========================================================================= + # SERVICE METHODS - Delegate to handler + # ========================================================================= + + async def service_set_tpi_parameters( + self, + tpi_coef_int: float | None = None, + tpi_coef_ext: float | None = None, + minimal_activation_delay: int | None = None, + minimal_deactivation_delay: int | None = None, + tpi_threshold_low: float | None = None, + tpi_threshold_high: float | None = None, + ): + """Service: set TPI parameters.""" + if hasattr(self._algo_handler, 'service_set_tpi_parameters'): + await self._algo_handler.service_set_tpi_parameters( + tpi_coef_int=tpi_coef_int, + tpi_coef_ext=tpi_coef_ext, + minimal_activation_delay=minimal_activation_delay, + minimal_deactivation_delay=minimal_deactivation_delay, + tpi_threshold_low=tpi_threshold_low, + tpi_threshold_high=tpi_threshold_high, + ) + else: + raise ServiceValidationError(f"{self} - This service is only available for TPI algorithm.") + + async def service_set_auto_tpi_mode( + self, + auto_tpi_mode: bool, + reinitialise: bool = True, + allow_kint_boost_on_stagnation: bool = False, + allow_kext_compensation_on_overshoot: bool = False, + ): + """Service: set Auto TPI mode.""" + if hasattr(self._algo_handler, 'service_set_auto_tpi_mode'): + await self._algo_handler.service_set_auto_tpi_mode( + auto_tpi_mode=auto_tpi_mode, + reinitialise=reinitialise, + allow_kint_boost_on_stagnation=allow_kint_boost_on_stagnation, + allow_kext_compensation_on_overshoot=allow_kext_compensation_on_overshoot, + ) + else: + raise ServiceValidationError(f"{self} - This service is only available for TPI algorithm.") + + async def service_auto_tpi_calibrate_capacity( + self, + save_to_config: bool, + min_power_threshold: int, + start_date=None, + end_date=None, + ): + """Service: calibrate Auto TPI capacity.""" + if hasattr(self._algo_handler, 'service_auto_tpi_calibrate_capacity'): + return await self._algo_handler.service_auto_tpi_calibrate_capacity( + save_to_config=save_to_config, + min_power_threshold=min_power_threshold, + start_date=start_date, + end_date=end_date, + ) + else: + raise ServiceValidationError(f"{self} - This service is only available for TPI algorithm.") + + async def async_set_auto_tpi_mode( + self, + auto_tpi_mode: bool, + reinitialise: bool = True, + allow_kint_boost: bool = False, + allow_kext_overshoot: bool = False, + ): + """Set the auto TPI mode.""" + if hasattr(self._algo_handler, 'async_set_auto_tpi_mode'): + await self._algo_handler.async_set_auto_tpi_mode( + auto_tpi_mode=auto_tpi_mode, + reinitialise=reinitialise, + allow_kint_boost=allow_kint_boost, + allow_kext_overshoot=allow_kext_overshoot, + ) + + def _bind_scheduler(self, scheduler) -> None: + """Store the CycleScheduler and notify the algo handler. + + Called by concrete subclasses (ThermostatOverSwitch, etc.) immediately + after CycleScheduler construction. The handler registers whatever + callbacks it needs via on_scheduler_ready() — the thermostat does not + need to know the details. + """ + self._cycle_scheduler = scheduler + if self._algo_handler: + self._algo_handler.on_scheduler_ready(scheduler) diff --git a/custom_components/versatile_thermostat/thermostat_switch.py b/custom_components/versatile_thermostat/thermostat_switch.py new file mode 100644 index 0000000..4e38320 --- /dev/null +++ b/custom_components/versatile_thermostat/thermostat_switch.py @@ -0,0 +1,226 @@ +# pylint: disable=line-too-long, abstract-method + +""" A climate over switch classe """ +import logging +from vtherm_api.log_collector import get_vtherm_logger +from datetime import timedelta +from homeassistant.core import Event, callback +from homeassistant.helpers.event import ( + async_track_state_change_event, + async_track_time_interval, + EventStateChangedData, +) +from homeassistant.core import HomeAssistant +from .vtherm_hvac_mode import VThermHvacMode_OFF + +from .const import ( + CONF_UNDERLYING_LIST, + CONF_HEATER_KEEP_ALIVE, + CONF_INVERSE_SWITCH, + CONF_VSWITCH_ON_CMD_LIST, + CONF_VSWITCH_OFF_CMD_LIST, + PROPORTIONAL_FUNCTION_TPI, + overrides, +) + +from .commons import write_event_log + +from .base_thermostat import BaseThermostat, ConfigData +from .thermostat_prop import ThermostatProp +from .underlyings import UnderlyingSwitch +from .cycle_scheduler import CycleScheduler + +_LOGGER = get_vtherm_logger(__name__) + +class ThermostatOverSwitch(ThermostatProp[UnderlyingSwitch]): + """Representation of a base class for a Versatile Thermostat over a switch.""" + + _entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access + frozenset( + { + "is_over_switch", + "vtherm_over_switch", + } + ) + ) + + def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None: + """Initialize the thermostat over switch.""" + self._is_inversed: bool | None = None + self._lst_vswitch_on: list[str] = [] + self._lst_vswitch_off: list[str] = [] + super().__init__(hass, unique_id, name, config_entry) + + @property + def is_over_switch(self) -> bool: + """True if the Thermostat is over_switch""" + return True + + @property + def is_inversed(self) -> bool: + """True if the switch is inversed (for pilot wire and diode)""" + return self._is_inversed is True + + @overrides + def post_init(self, config_entry: ConfigData): + """Initialize the Thermostat""" + + super().post_init(config_entry) + + self._is_inversed = config_entry.get(CONF_INVERSE_SWITCH) is True + + lst_switches = config_entry.get(CONF_UNDERLYING_LIST) + self._lst_vswitch_on = config_entry.get(CONF_VSWITCH_ON_CMD_LIST, []) + self._lst_vswitch_off = config_entry.get(CONF_VSWITCH_OFF_CMD_LIST, []) + + for idx, switch in enumerate(lst_switches): + vswitch_on = self._lst_vswitch_on[idx] if idx < len(self._lst_vswitch_on) else None + vswitch_off = self._lst_vswitch_off[idx] if idx < len(self._lst_vswitch_off) else None + self._underlyings.append( + UnderlyingSwitch( + hass=self._hass, + thermostat=self, + switch_entity_id=switch, + keep_alive_sec=config_entry.get(CONF_HEATER_KEEP_ALIVE, 0), + vswitch_on=vswitch_on, + vswitch_off=vswitch_off, + ) + ) + + self._bind_scheduler(CycleScheduler( + hass=self._hass, + thermostat=self, + underlyings=self._underlyings, + cycle_duration_sec=self._cycle_min * 60, + min_activation_delay=self.minimal_activation_delay, + min_deactivation_delay=self.minimal_deactivation_delay, + )) + + self._should_relaunch_control_heating = False + + @overrides + async def async_added_to_hass(self): + """Run when entity about to be added.""" + + await super().async_added_to_hass() + + # Add listener to all underlying entities + for switch in self._underlyings: + self.async_on_remove(async_track_state_change_event(self.hass, [switch.entity_id], self._async_switch_changed)) + + # Start the control_heating + # starts a cycle + self.async_on_remove( + async_track_time_interval( + self.hass, + self.async_control_heating, + interval=timedelta(minutes=self._cycle_min), + ) + ) + + @overrides + def update_custom_attributes(self): + """Custom attributes""" + super().update_custom_attributes() + + under0: UnderlyingSwitch = self._underlyings[0] + + # Standard attributes + attributes = { + "is_over_switch": self.is_over_switch, + "on_percent": self.safe_on_percent, + "power_percent": self.power_percent, + } + + # Use pre-calculated on/off times from handler's control_heating + on_time_sec = self._on_time_sec + off_time_sec = self._off_time_sec + + # Underlying specific attributes + vtherm_over_switch_attr = { + "is_inversed": self.is_inversed, + "keep_alive_sec": under0.keep_alive_sec, + "underlying_entities": [underlying.entity_id for underlying in self._underlyings], + "on_percent": self.safe_on_percent, + "power_percent": self.power_percent, + "on_time_sec": on_time_sec, + "off_time_sec": off_time_sec, + "function": self._proportional_function, + "vswitch_on_commands": self._lst_vswitch_on, + "vswitch_off_commands": self._lst_vswitch_off, + } + + # Add TPI attributes if active + if self._proportional_function == PROPORTIONAL_FUNCTION_TPI: + vtherm_over_switch_attr.update({ + "tpi_coef_int": self._tpi_coef_int, + "tpi_coef_ext": self._tpi_coef_ext, + "tpi_threshold_low": self._tpi_threshold_low, + "tpi_threshold_high": self._tpi_threshold_high, + "minimal_activation_delay": self._minimal_activation_delay, + "minimal_deactivation_delay": self._minimal_deactivation_delay, + }) + + attributes["vtherm_over_switch"] = vtherm_over_switch_attr + self._attr_extra_state_attributes.update(attributes) + + # _LOGGER.debug("%s - Calling update_custom_attributes: %s", self, self._attr_extra_state_attributes) + + @overrides + def incremente_energy(self): + """increment the energy counter if device is active""" + if self.vtherm_hvac_mode == VThermHvacMode_OFF: + return + + added_energy = 0 + if self.power_manager.mean_cycle_power is not None: + # each underlying entity calculate its own energy. So we should divide by the number of underlying entities + # see #877 + added_energy = self.power_manager.mean_cycle_power * float(self._cycle_min) / 60.0 / self.nb_underlying_entities + + 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() + self.async_write_ha_state() + + _LOGGER.debug( + "%s - added energy is %.3f . Total energy is now: %.3f", + self, + added_energy, + self._total_energy, + ) + + @callback + def _async_switch_changed(self, event: Event[EventStateChangedData]): + """Handle heater switch state changes.""" + new_state = event.data.get("new_state") + old_state = event.data.get("old_state") + + write_event_log(_LOGGER, self, f"Underlying switch state changed from {old_state.state if old_state else None} to {new_state.state if new_state else None}") + if new_state is None: + return + # #1654 - nno more needed now + # if old_state is None: + # self.hass.create_task(self._check_initial_state()) + + self.calculate_hvac_action() + self.update_custom_attributes() + self.async_write_ha_state() + + @property + def vtherm_type(self) -> str | None: + """Return the type of thermostat""" + return "over_switch" diff --git a/custom_components/versatile_thermostat/thermostat_valve.py b/custom_components/versatile_thermostat/thermostat_valve.py new file mode 100644 index 0000000..1395989 --- /dev/null +++ b/custom_components/versatile_thermostat/thermostat_valve.py @@ -0,0 +1,313 @@ +# pylint: disable=line-too-long, abstract-method +""" A climate over switch classe """ +import logging +from vtherm_api.log_collector import get_vtherm_logger +from datetime import timedelta, datetime + +from homeassistant.helpers.event import ( + async_track_state_change_event, + async_track_time_interval, + async_call_later, + EventStateChangedData, +) +from homeassistant.core import Event, HomeAssistant, callback + +from .base_thermostat import BaseThermostat, ConfigData +from .thermostat_prop import ThermostatProp + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons import write_event_log + +from .underlyings import UnderlyingValve +from .cycle_scheduler import CycleScheduler +from .vtherm_hvac_mode import VThermHvacMode_OFF + +_LOGGER = get_vtherm_logger(__name__) + +class ThermostatOverValve(ThermostatProp[UnderlyingValve]): # pylint: disable=abstract-method + """Representation of a class for a Versatile Thermostat over a Valve""" + + _entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access + frozenset( + { + "is_over_valve", + "vtherm_over_valve", + } + ) + ) + + def __init__( + self, hass: HomeAssistant, unique_id: str, name: str, config_entry: ConfigData + ): + """Initialize the thermostat over switch.""" + self._valve_open_percent: int = 0 + self._last_calculation_timestamp: datetime | None = None + self._auto_regulation_dpercent: float | None = None + self._auto_regulation_period_min: int | None = None + + # Call to super must be done after initialization because it calls post_init at the end + super().__init__(hass, unique_id, name, config_entry) + + @property + def is_over_valve(self) -> bool: + """True if the Thermostat is over_valve""" + return True + + @property + def valve_open_percent(self) -> int: + """Gives the percentage of valve needed""" + if self.vtherm_hvac_mode is VThermHvacMode_OFF: + return 0 + else: + return self._valve_open_percent + + @overrides + def post_init(self, config_entry: ConfigData): + """Initialize the Thermostat""" + + 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 + ) + + lst_valves = config_entry.get(CONF_UNDERLYING_LIST) + + for _, valve in enumerate(lst_valves): + self._underlyings.append( + UnderlyingValve(hass=self._hass, thermostat=self, valve_entity_id=valve) + ) + + self._bind_scheduler(CycleScheduler( + hass=self._hass, + thermostat=self, + underlyings=self._underlyings, + cycle_duration_sec=self._cycle_min * 60, + min_activation_delay=self.minimal_activation_delay, + min_deactivation_delay=self.minimal_deactivation_delay, + )) + + self._should_relaunch_control_heating = False + + @overrides + async def async_added_to_hass(self): + """Run when entity about to be added.""" + + await super().async_added_to_hass() + + # Add listener to all underlying entities + for valve in self._underlyings: + self.async_on_remove( + async_track_state_change_event( + self.hass, [valve.entity_id], self._async_valve_changed + ) + ) + + # Start the control_heating + # starts a cycle + self.async_on_remove( + async_track_time_interval( + self.hass, + self.async_control_heating, + interval=timedelta(minutes=self._cycle_min), + ) + ) + + @callback + async def _async_valve_changed(self, event: Event[EventStateChangedData]): + """Handle unerdlying valve state changes. + This method just log the change. It changes nothing to avoid loops. + """ + new_state = event.data.get("new_state") + self.calculate_hvac_action() + self.update_custom_attributes() + self.async_write_ha_state() + write_event_log(_LOGGER, self, f"Underlying valve state changed to {new_state}") + + @overrides + def update_custom_attributes(self): + """Custom attributes""" + super().update_custom_attributes() + + self._attr_extra_state_attributes.update( + { + "is_over_valve": self.is_over_valve, + "on_percent": self.safe_on_percent, + "power_percent": self.power_percent, + "valve_open_percent": self.valve_open_percent, + "vtherm_over_valve": { + "valve_open_percent": self.valve_open_percent, + "underlying_entities": [underlying.entity_id for underlying in self._underlyings], + "on_percent": self.safe_on_percent, + "function": self._proportional_function, + "tpi_coef_int": self._tpi_coef_int, + "tpi_coef_ext": self._tpi_coef_ext, + "tpi_threshold_low": self._tpi_threshold_low, + "tpi_threshold_high": self._tpi_threshold_high, + "minimal_activation_delay": self._minimal_activation_delay, + "minimal_deactivation_delay": self._minimal_deactivation_delay, + "auto_regulation_dpercent": self._auto_regulation_dpercent, + "auto_regulation_period_min": self._auto_regulation_period_min, + "last_calculation_timestamp": (self._last_calculation_timestamp.astimezone(self._current_tz).isoformat() if self._last_calculation_timestamp else None), + "calculated_on_percent": ( + self._prop_algorithm.calculated_on_percent + if self._prop_algorithm + else None + ), + }, + } + ) + + # _LOGGER.debug("%s - Calling update_custom_attributes: %s", self, self._attr_extra_state_attributes) + + @overrides + def recalculate(self, force=False): + """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) + + self.stop_recalculate_later() + + if self._auto_regulation_period_min is None or self._auto_regulation_dpercent is None: + _LOGGER.warning( + "%s - auto_regulation_period_min or auto_regulation_dpercent is not set. Stopping TPI calculation.", + self, + ) + return + + # 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 not force and period < self._auto_regulation_period_min: + _LOGGER.info( + "%s - do not calculate TPI because regulation_period (%d) is not exceeded", + self, + period, + ) + + self.do_recalculate_later() + return + + if self._prop_algorithm: + self._prop_algorithm.calculate( + self.target_temperature, + self._cur_temp, + self._cur_ext_temp, + self.last_temperature_slope, + self.vtherm_hvac_mode or VThermHvacMode_OFF, + ) + + current_on_percent = self.safe_on_percent + if current_on_percent is None: + # Temperature not yet available; preserve the current valve position. + return + + new_valve_percent = round( + max(0, min(current_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 ( + 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._valve_open_percent == new_valve_percent: + _LOGGER.debug("%s - no change in valve_open_percent.", self) + return + + self._valve_open_percent = new_valve_percent + + # Valve open percent is sent to underlyings by CycleScheduler + # in start_cycle (called from control_heating) + + self._last_calculation_timestamp = now + + # self.calculate_hvac_action() + self.update_custom_attributes() + self.async_write_ha_state() + + def do_recalculate_later(self): + """A utility function to set the valve open percent later on all underlyings""" + _LOGGER.debug("%s - do_recalculate_later call", self) + + async def callback_recalculate(_): + """Callback to set the valve percent""" + self.recalculate() + current_on_percent = self.safe_on_percent + if self._cycle_scheduler and current_on_percent is not None: + await self._cycle_scheduler.apply_valve_update( + self.vtherm_hvac_mode or VThermHvacMode_OFF, + current_on_percent, + ) + self.update_custom_attributes() + self.async_write_ha_state() + + self.stop_recalculate_later() + + self._cancel_recalculate_later = async_call_later(self._hass, delay=20, action=callback_recalculate) + + @overrides + def incremente_energy(self): + """increment the energy counter if device is active""" + if self.vtherm_hvac_mode == VThermHvacMode_OFF: + return + + added_energy = 0 + if not self.is_over_climate and self.power_manager.mean_cycle_power is not None: + added_energy = ( + self.power_manager.mean_cycle_power * float(self._cycle_min) / 60.0 + ) + + 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() + self.async_write_ha_state() + + _LOGGER.debug( + "%s - added energy is %.3f . Total energy is now: %.3f", + self, + added_energy, + self._total_energy, + ) + + @property + def vtherm_type(self) -> str | None: + """Return the type of thermostat""" + return "over_valve" diff --git a/custom_components/versatile_thermostat/translations/cs.json b/custom_components/versatile_thermostat/translations/cs.json new file mode 100644 index 0000000..9ef01c1 --- /dev/null +++ b/custom_components/versatile_thermostat/translations/cs.json @@ -0,0 +1,1049 @@ +{ + "title": "Konfigurace Versatile Thermostat", + "config": { + "flow_title": "Konfigurace Versatile Thermostat", + "step": { + "user": { + "title": "Typ Versatile Thermostatu", + "description": "Vyberte typ termostatu, který chcete vytvořit [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/creation.md)", + "data": { + "thermostat_type": "Typ termostatu" + }, + "data_description": { + "thermostat_type": "Možný je pouze jeden termostat typu Centrální konfigurace." + } + }, + "menu": { + "title": "Nabídka", + "description": "Nakonfigurujte váš termostat. Konfiguraci budete moci dokončit až po zadání všech požadovaných parametrů.", + "menu_options": { + "main": "Hlavní atributy", + "central_boiler": "Centrální kotel", + "type": "Podřízená zařízení", + "tpi": "TPI parametry", + "features": "Funkce", + "presets": "Předvolby", + "window": "Detekce oken", + "motion": "Detekce pohybu", + "power": "Správa výkonu", + "presence": "Detekce přítomnosti", + "advanced": "Pokročilé parametry", + "auto_start_stop": "Automatické spuštění a zastavení", + "valve_regulation": "Konfigurace regulace ventilu", + "sync_device_internal_temp": "Synchronizace teploty zařízení", + "heating_failure_detection": "Detekce poruchy vytápění", + "lock": "Správa zámku", + "finalize": "Vše hotovo", + "configuration_not_complete": "Konfigurace není kompletní", + "auto_tpi_configuration": "Auto TPI - Konfigurace", + "auto_tpi_avg_settings": "Auto TPI - Vážený průměr", + "auto_tpi_ema_settings": "Auto TPI - EWMA nastavení" + } + }, + "lock": { + "title": "Správa zamknutí", + "description": "Funkce zamknutí brání změnám termostatu z uživatelského rozhraní nebo automatizací při zachování provozuschopnosti termostatu.", + "data": { + "use_lock_central_config": "Použít centrální konfiguraci zamknutí", + "lock_code": "Volitelný PIN (4 číslice)", + "lock_users": "Zamknutí zabrání příkazům uživatelů", + "lock_automations": "Zamknutí zabrání příkazům z automatizací a integrací (např. plánovač)", + "auto_relock_sec": "Automatické opětovné zamknutí" + }, + "data_description": { + "use_lock_central_config": "Zaškrtněte pro použití centrální konfigurace zamknutí. Odškrtněte pro použití konfigurace zamknutí specifické pro tento VTherm", + "auto_relock_sec": "Prodleva v sekundách před automatickým opětovným zamknutím po odemknutí. Nastavte 0 pro deaktivaci. Výchozí hodnota je 30 sekund." + } + }, + "main": { + "title": "Přidat nový Versatile Thermostat", + "description": "Hlavní povinné atributy [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/base-attributes.md)", + "data": { + "name": "Název", + "thermostat_type": "Typ termostatu", + "temperature_sensor_entity_id": "Teplota místnosti", + "last_seen_temperature_sensor_entity_id": "Čas posledního měření teploty místnosti", + "external_temperature_sensor_entity_id": "ID entity senzoru venkovní teploty", + "cycle_min": "Délka cyklu (minuty)", + "temp_min": "Minimální povolená teplota", + "temp_max": "Maximální povolená teplota", + "step_temperature": "Krok teploty", + "device_power": "Výkon zařízení", + "use_central_mode": "Povolit řízení centrální entitou (vyžaduje centrální konfiguraci). Zaškrtněte pro povolení řízení VTherm pomocí entit select central_mode.", + "use_main_central_config": "Použít dodatečnou centrální hlavní konfiguraci. Zaškrtněte pro použití centrální hlavní konfigurace (venkovní teplota, min, max, krok, ...).", + "used_by_controls_central_boiler": "Používán centrálním kotlem. Zaškrtněte, pokud má tento VTherm řídit centrální kotel" + }, + "data_description": { + "cycle_min": "Minimální délka jednoho cyklu vytápění/chlazení v minutách, aby se zabránilo příliš častému přepínání", + "temp_min": "Minimální povolená teplota pro termostat", + "temp_max": "Maximální povolená teplota pro termostat", + "step_temperature": "Krok teploty pro nastavení cílové teploty", + "temperature_sensor_entity_id": "ID entity senzoru teploty místnosti", + "last_seen_temperature_sensor_entity_id": "ID entity posledního času měření teploty místnosti. Měl by být datetime senzor", + "external_temperature_sensor_entity_id": "ID entity senzoru venkovní teploty. Nepoužívá se, pokud je zvolena centrální konfigurace" + } + }, + "features": { + "title": "Funkce", + "description": "Funkce termostatu", + "data": { + "use_window_feature": "Použít detekci oken", + "use_motion_feature": "Použít detekci pohybu", + "use_power_feature": "Použít správu výkonu", + "use_presence_feature": "Použít detekci přítomnosti", + "use_central_boiler_feature": "Použít centrální kotel. Zaškrtněte pro přidání ovládání centrálního kotle. Po zaškrtnutí budete muset nakonfigurovat VTherm, který bude mít kontrolu nad centrálním kotlem. Pokud jeden VTherm vyžaduje vytápění, kotel se zapne. Pokud žádný VTherm nevyžaduje vytápění, kotel se vypne. Příkazy pro zapnutí/vypnutí centrálního kotle jsou uvedeny na související konfigurační stránce", + "use_auto_start_stop_feature": "Použít funkci automatického spuštění a zastavení", + "use_heating_failure_detection_feature": "Použít detekci poruchy vytápění" + } + }, + "type": { + "title": "Propojené entity", + "description": "Atributy propojených entit [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/over-switch.md#konfigurace)", + "data": { + "underlying_entity_ids": "Zařízení, která mají být řízena", + "heater_keep_alive": "Interval keep-alive spínače v sekundách", + "proportional_function": "Algoritmus", + "ac_mode": "AC režim", + "sync_device_internal_temp": "Synchronizovat vnitřní teplotu zařízení", + "auto_regulation_mode": "Samoregulace", + "auto_regulation_dtemp": "Práh regulace", + "auto_regulation_periode_min": "Minimální perioda regulace", + "auto_regulation_use_device_temp": "Použít vnitřní teplotu podřízeného zařízení", + "inverse_switch_command": "Invertovat příkaz spínače", + "auto_fan_mode": "Automatický režim ventilátoru", + "on_command_text": "Přizpůsobení příkazu zapnutí", + "vswitch_on_command": "Volitelné příkazy zapnutí", + "off_command_text": "Přizpůsobení příkazu vypnutí", + "vswitch_off_command": "Volitelné příkazy vypnutí" + }, + "data_description": { + "underlying_entity_ids": "Zařízení, která mají být řízena - 1 je povinné", + "heater_keep_alive": "Volitelný interval obnovení stavu spínače vytápění. Ponechte prázdné, pokud není potřeba.", + "proportional_function": "Algoritmus k použití (další algoritmy lze nainstalovat jako {plugins_link})", + "ac_mode": "Použít režim klimatizace (AC)", + "sync_device_internal_temp": "Synchronizovat vnitřní teplotu podřízených zařízení s vybranými entitami", + "auto_regulation_mode": "Automatické přizpůsobení cílové teploty", + "auto_regulation_dtemp": "Práh v ° (nebo % pro ventil), pod kterým nebude změna teploty odeslána", + "auto_regulation_periode_min": "Doba v minutách mezi dvěma aktualizacemi regulace", + "auto_regulation_use_device_temp": "Použít případný vnitřní teplotní senzor podřízeného zařízení pro zrychlení samoregulace", + "inverse_switch_command": "Pro spínače s pilotem a diodou možná budete muset invertovat příkaz", + "auto_fan_mode": "Automaticky aktivovat ventilátor při nutnosti velkého vytápění/chlazení", + "on_command_text": "Pro podřízená zařízení typu `select` nebo `climate` musíte přizpůsobit příkazy." + } + }, + "tpi": { + "title": "TPI parametry", + "description": "Atributy Time Proportional Integral [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/algorithms.md#algoritmus-tpi)", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "use_tpi_central_config": "Použít centrální TPI konfiguraci", + "tpi_threshold_low": "Nízký práh TPI", + "tpi_threshold_high": "Vysoký práh TPI", + "minimal_activation_delay": "Minimální zpoždění aktivace", + "minimal_deactivation_delay": "Minimální zpoždění deaktivace", + "auto_tpi_mode": "Povolit automatické učení TPI" + }, + "data_description": { + "tpi_coef_int": "Koeficient pro delta vnitřní teploty", + "tpi_coef_ext": "Koeficient pro delta vnější teploty", + "use_tpi_central_config": "Zaškrtněte pro použití centrální TPI konfigurace. Odšktněte pro použití specifické TPI konfigurace pro tento VTherm", + "tpi_threshold_low": "Práh ve °, pod kterým bude algoritmus TPI zapnutý. 0 znamená žádný práh", + "tpi_threshold_high": "Práh ve °, nad kterým bude algoritmus TPI vypnutý. 0 znamená žádný práh", + "minimal_activation_delay": "Zpoždění v sekundách, při kterém se zařízení neaktivuje", + "minimal_deactivation_delay": "Prodleva v sekundách, po kterou bude zařízení udržováno aktivní", + "auto_tpi_mode": "Učení TPI se spouští ručně, nejprve si přečtěte dokument." + } + }, + "presets": { + "title": "Předvolby", + "description": "Vyberte, zda bude termostat používat centrální předvolby - odškrtněte pro vlastní předvolby termostatu [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-presets.md)", + "data": { + "use_presets_central_config": "Použít centrální konfiguraci předvoleb" + } + }, + "window": { + "title": "Správa oken", + "description": "Správa otevřených oken.\nMůžete také nakonfigurovat automatickou detekci otevřených oken založenou na poklesu teploty [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-window.md)", + "data": { + "window_sensor_entity_id": "ID entity senzoru okna", + "window_delay": "Zpoždění 'zapnuto' senzoru okna (sekundy)", + "window_off_delay": "Zpoždění 'vypnuto' senzoru okna (sekundy)", + "window_auto_open_threshold": "Práh poklesu teploty pro automatickou detekci otevřeného okna (v °/hodina)", + "window_auto_close_threshold": "Práh zvýšení teploty pro ukončení automatické detekce (v °/hodina)", + "window_auto_max_duration": "Maximální doba automatické detekce otevřeného okna (v min)", + "use_window_central_config": "Použít centrální konfiguraci oken", + "window_action": "Akce" + }, + "data_description": { + "window_sensor_entity_id": "Ponechte prázdné, pokud se nemá používat senzor okna a má se používat automatická detekce", + "window_delay": "Zpoždění v sekundách před zohledněním detekce 'zapnuto' senzoru", + "window_off_delay": "Zpoždění v sekundách před zohledněním detekce 'vypnuto' senzoru. Ponechte prázdné pro použití stejné hodnoty jako zpoždění zapnutí okna", + "window_auto_open_threshold": "Doporučená hodnota: mezi 3 a 10. Ponechte prázdné, pokud se automatická detekce otevřeného okna nepoužívá", + "window_auto_close_threshold": "Doporučená hodnota: 0. Ponechte prázdné, pokud se automatická detekce otevřeného okna nepoužívá", + "window_auto_max_duration": "Doporučená hodnota: 60 (jedna hodina). Ponechte prázdné, pokud se automatická detekce otevřeného okna nepoužívá", + "use_window_central_config": "Vyberte pro použití centrální konfigurace oken. Odškrtněte pro použití specifické konfigurace oken pro tento VTherm", + "window_action": "Akce, která se má provést při detekci otevřeného okna" + } + }, + "motion": { + "title": "Správa pohybu", + "description": "Správa senzoru pohybu. Předvolba se může automaticky přepínat v závislosti na detekci pohybu\npředvolba při pohybu (motion_preset) a předvolba bez pohybu (no_motion_preset) by měly být nastaveny na odpovídající název předvolby [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-motion.md)", + "data": { + "motion_sensor_entity_id": "ID entity senzoru pohybu", + "motion_delay": "Zpoždění aktivace", + "motion_off_delay": "Zpoždění deaktivace", + "motion_preset": "Předvolba pohybu", + "no_motion_preset": "Předvolba bez pohybu", + "use_motion_central_config": "Použít centrální konfiguraci pohybu" + }, + "data_description": { + "motion_sensor_entity_id": "ID entity senzoru pohybu", + "motion_delay": "Zpoždění aktivace pohybu (sekundy)", + "motion_off_delay": "Zpoždění deaktivace pohybu (sekundy)", + "motion_preset": "Předvolba k použití při detekci pohybu", + "no_motion_preset": "Předvolba k použití při nedetekci pohybu", + "use_motion_central_config": "Zaškrtněte pro použití centrální konfigurace pohybu. Odškrtněte pro použití specifické konfigurace pohybu pro tento VTherm" + } + }, + "power": { + "title": "Správa výkonu", + "description": "Atributy správy výkonu.\nUdává výkon a maximální výkonový senzor vašeho domova.\nUveďte spotřebu energie ohřívače při zapnutí.\nVšechny senzory a výkon zařízení by měly používat stejnou jednotku (kW nebo W) [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-power.md)", + "data": { + "power_sensor_entity_id": "Výkon", + "max_power_sensor_entity_id": "Maximální výkon", + "power_temp": "Teplota odlehčení", + "use_power_central_config": "Použít centrální konfiguraci výkonu" + }, + "data_description": { + "power_sensor_entity_id": "ID entity senzoru výkonu", + "max_power_sensor_entity_id": "ID entity senzoru maximálního výkonu", + "power_temp": "Teplota pro odlehčení výkonu", + "use_power_central_config": "Zaškrtněte pro použití centrální konfigurace výkonu. Odškrtněte pro použití specifické konfigurace výkonu pro tento VTherm" + } + }, + "presence": { + "title": "Správa přítomnosti", + "description": "Atributy správy přítomnosti.\nUdává senzor přítomnosti vašeho domova (true znamená, že je někdo přítomen) a dává odpovídající nastavení teplotní předvolby [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-presence.md)", + "data": { + "presence_sensor_entity_id": "Senzor přítomnosti", + "use_presence_central_config": "Použít centrální konfiguraci teploty přítomnosti. Odškrtněte pro použití specifických teplotních entit" + }, + "data_description": { + "presence_sensor_entity_id": "ID entity senzoru přítomnosti" + } + }, + "advanced": { + "title": "Pokročilé parametry", + "description": "Konfigurace pokročilých parametrů. Pokud nevíte, co děláte, ponechte výchozí hodnoty.\nTyto parametry mohou vést k velmi špatné regulaci teploty nebo špatné regulaci výkonu [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-advanced.md)", + "data": { + "safety_delay_min": "Bezpečnostní zpoždění (v minutách)", + "safety_min_on_percent": "Minimální procento výkonu pro povolení bezpečnostního režimu", + "safety_default_on_percent": "Procento výkonu k použití v bezpečnostním režimu", + "repair_incorrect_state": "Opravit nesprávný stav", + "use_advanced_central_config": "Použít centrální pokročilou konfiguraci" + }, + "data_description": { + "safety_delay_min": "Maximální povolené zpoždění v minutách mezi dvěma měřeními teploty. Nad tímto zpožděním se termostat přepne do bezpečnostního vypnutého stavu", + "safety_min_on_percent": "Minimální hodnota procenta vytápění pro aktivaci bezpečnostní předvolby. Pod tímto množstvím procent výkonu se termostat nepřepne do bezpečnostní předvolby", + "safety_default_on_percent": "Výchozí hodnota procenta výkonu vytápění v bezpečnostní předvolbě. Nastavte na 0 pro vypnutí ohřívače v bezpečnostní předvolbě", + "repair_incorrect_state": "Automaticky opravit nesprávný stav základních entit při zjištění. Funkce znovu pošle požadovaný příkaz, pokud stav základní entity neodpovídá očekávanému stavu.", + "use_advanced_central_config": "Zaškrtněte pro použití centrální pokročilé konfigurace. Odškrtněte pro použití specifické pokročilé konfigurace pro tento VTherm" + } + }, + "central_boiler": { + "title": "Ovládání centrálního kotle", + "description": "Zadejte služby, které se mají volat pro zapnutí/vypnutí centrálního kotle. Volaná služba musí být naformátována následovně: `entity_id/service_name[/attribute:value]` (/attribute:value je volitelné)\nNapříklad:\n- pro zapnutí spínače: `switch.controle_chaudiere/switch.turn_on`\n- pro vypnutí spínače: `switch.controle_chaudiere/switch.turn_off`\n- pro naprogramování kotle na 25° a tím vynucení jeho zapálení: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- pro poslání 10° do kotle a tím vynucení jeho vypnutí: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10` [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-central-boiler.md)", + "data": { + "central_boiler_activation_service": "Příkaz pro zapnutí", + "central_boiler_deactivation_service": "Příkaz pro vypnutí", + "central_boiler_activation_delay_sec": "Zpoždění aktivace (v sekundách)", + "keep_alive_boiler_delay_sec": "Zpoždění keep-alive (sekundy)" + }, + "data_description": { + "central_boiler_activation_service": "Příkaz pro zapnutí centrálního kotle naformátovaný jako entity_id/service_name[/attribut:hodnota]", + "central_boiler_deactivation_service": "Příkaz pro vypnutí centrálního kotle naformátovaný jako entity_id/service_name[/attribut:hodnota]", + "central_boiler_activation_delay_sec": "Zpoždění aktivace (v sekundách) před zapnutím centrálního kotle, když je potřeba jej zapnout", + "keep_alive_boiler_delay_sec": "Zpoždění v sekundách pro periodické opakování příkazu kotle, pokud je keep-alive povoleno. Nastavte na 0, aby se keep-alive zakázal." + } + }, + "valve_regulation": { + "title": "Samoregulace s ventilem", + "description": "Konfigurace pro samoregulaci s přímým ovládáním ventilu [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/self-regulation.md#samo-regulace-přímým-ovládáním-ventilu)", + "data": { + "offset_calibration_entity_ids": "Entita kalibrace offsetu", + "opening_degree_entity_ids": "Entita stupně otevření", + "closing_degree_entity_ids": "Entita stupně zavření", + "proportional_function": "Algoritmus", + "opening_threshold_degree": "Prahová hodnota otevření", + "min_opening_degrees": "Minimální stupně otevření", + "max_opening_degrees": "Maximální stupeň otevření", + "max_closing_degree": "Maximální stupeň zavření" + }, + "data_description": { + "offset_calibration_entity_ids": "Seznam entit 'kalibrace offsetu'. Nastavte, pokud má váš TRV entitu pro lepší regulaci. Měla by být jedna na každou podřízenou klimatizační entitu", + "opening_degree_entity_ids": "Seznam entit 'stupně otevření'. Měla by být jedna na každou podřízenou klimatizační entitu", + "closing_degree_entity_ids": "Seznam entit 'stupně zavření'. Nastavte, pokud má váš TRV entitu pro lepší regulaci. Měla by být jedna na každou podřízenou klimatizační entitu", + "proportional_function": "Algoritmus k použití (další algoritmy lze nainstalovat jako {plugins_link})", + "opening_threshold_degree": "Hodnota otevření ventilu, pod kterou by měl být ventil považován za zavřený (a pak bude aplikován 'max_closing_degree')", + "min_opening_degrees": "Minimální hodnota stupně otevření pro každé podřízené zařízení, oddělené čárkami. Výchozí je 0. Příklad: 20, 25, 30", + "max_opening_degrees": "Maximální hodnota stupně otevření pro každé základní zařízení, oddělené čárkami. Výchozí hodnota je 100. Ventil se nikdy neotevře více než tato hodnota. Příklad: 80, 85, 90", + "max_closing_degree": "Maximální hodnota stupně zavření. Ventil nebude nikdy uzavřen více než tato hodnota. Nastavte na 100 pro úplné zavření ventilu" + } + }, + "sync_device_internal_temp": { + "title": "Synchronizujte vnitřní teplotu zařízení", + "description": "Konfigurace pro synchronizaci vnitřní teploty základních zařízení s vybranými entitami", + "data": { + "sync_with_calibration": "Použít kalibraci offsetu", + "sync_entity_ids": "ID entity použité pro synchronizaci" + }, + "data_description": { + "sync_with_calibration": "Zaškrtněte, chcete-li použít kalibraci offsetu při synchronizaci vnitřní teploty. Zrušte zaškrtnutí, chcete-li přímo zkopírovat teplotu z vybraných entit", + "sync_entity_ids": "Seznam entit používaných k synchronizaci vnitřní teploty základních zařízení. Na každé základní zařízení by měl být jeden. Pokud je zaškrtnuto políčko, měla by to být kalibrační entita nebo jinak entita teploty. Oba jsou entity typu „číslo“." + } + }, + "heating_failure_detection": { + "title": "Detekce poruchy vytápění", + "description": "Nakonfigurujte funkci detekce poruchy vytápění. To detekuje anomálie, když se očekává ohřev, ale teplota se nezvyšuje, nebo když je topení vypnuté, ale teplota stále stoupá.", + "data": { + "use_heating_failure_detection_feature": "Povolit detekci poruchy vytápění", + "use_heating_failure_detection_central_config": "Použít centrální konfiguraci detekce poruchy vytápění", + "heating_failure_threshold": "Prahová hodnota poruchy vytápění", + "cooling_failure_threshold": "Prahová hodnota poruchy chlazení", + "heating_failure_detection_delay": "Zpoždění detekce (minuty)", + "temperature_change_tolerance": "Tolerance změny teploty (°C)", + "failure_detection_enable_template": "Šablona povolení detekce" + }, + "data_description": { + "use_heating_failure_detection_feature": "Povolte detekci anomálií ohřevu pro VTherm s TPI", + "use_heating_failure_detection_central_config": "Zaškrtněte pro použití centrální konfigurace detekce poruchy vytápění. Odškrtněte pro použití specifické konfigurace pro tento VTherm", + "heating_failure_threshold": "Prahová hodnota on_percent nad kterou by mělo zahřívání způsobit zvýšení teploty (0,9 = 90 %)", + "cooling_failure_threshold": "Prahová hodnota on_percent pod kterou by se teplota neměla zvýšit (0,0 = 0 %)", + "heating_failure_detection_delay": "Doba v minutách před zkontrolováním, zda se teplota změnila podle očekávání", + "temperature_change_tolerance": "Minimální změna teploty ve stupních, kterou je třeba považovat za významnou. Menší změny jsou ignorovány pro filtrování šumu senzoru (výchozí: 0,5 °C)", + "failure_detection_enable_template": "An optional Jinja2 template that must return True to enable detection. Useful to temporarily disable detection when an external heat source is active (e.g., is_state('binary_sensor.wood_stove', 'off') in double curly braces). If empty, detection is always enabled." + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - Konfigurace", + "description": "Obecná nastavení pro automatické učení TPI.", + "data": { + "auto_tpi_learning_type": "Typ učení", + "heater_heating_time": "Doba ohřevu (min)", + "heater_cooling_time": "Doba chlazení (min)", + "auto_tpi_heating_rate": "Rychlost ohřevu ({unit}/h)", + "auto_tpi_aggressiveness": "Agresivita", + "auto_tpi_enable_advanced_settings": "Povolit pokročilé parametry", + "auto_tpi_continuous_kext": "Průběžné učení Kext" + }, + "data_description": { + "auto_tpi_learning_type": "Zvolte 'Discovery' pro počáteční spuštění (vážený průměr, váha 1) nebo 'Fine Tuning' pro průběžné úpravy (EWMA, Alpha 0,08).", + "heater_heating_time": "Čas k dosažení plného výkonu (minuty)", + "heater_cooling_time": "Čas na ochlazení po zastavení (minuty)\n\n| Typ | Doba ohřevu | Doba chlazení |\n| :--- | :--- | :--- |\n| Elektrický radiátor | 5 min | 7mn |\n| Vodní radiátor | 15 minut | 20 min |\n| Podlahové vytápění | 30 min | 45 min |", + "auto_tpi_heating_rate": "Kapacita nárůstu teploty radiátoru ({unit} za hodinu). Ponechejte na 0 pro automatické učení (bootstrap).", + "auto_tpi_aggressiveness": "Redukční faktor pro naučené koeficienty (50-100%). Nižší hodnoty vedou ke konzervativnějším koeficientům a snižují riziko přestřelení.", + "auto_tpi_enable_advanced_settings": "Zaškrtněte pro přístup k jemnému doladění parametrů (Alpha, Decay, Weight).", + "auto_tpi_continuous_kext": "Zaškrtnutím povolíte průběžné učení externího koeficientu (Kext) mimo relace AutoTPI." + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - Vážený průměr", + "description": "Parametry pro metodu váženého průměru.", + "data": { + "auto_tpi_avg_initial_weight": "Počáteční hmotnost" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Váha počáteční/předchozí hodnoty ve výpočtu průměru" + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA nastavení", + "description": "Parametry pro metodu EWMA.\n\nDoporučení:\n| Situace | Alfa (ema_alpha) | Decay Rate (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Počáteční učení | 0,15 | 0,08 |\n| Jemné učení | 0,08 | 0,12 |\n| Průběžné učení | 0,05 | 0,02 |", + "data": { + "auto_tpi_ema_alpha": "Alfa", + "auto_tpi_ema_decay_rate": "Míra rozpadu", + "auto_tpi_continuous_kext_alpha": "Kontinuální Kext Alpha" + }, + "data_description": { + "auto_tpi_ema_alpha": "Faktor vyhlazování (0-1). Vyšší = rychlejší adaptace", + "auto_tpi_ema_decay_rate": "Rychlost, se kterou alfa klesá v průběhu času (stabilizace)", + "auto_tpi_continuous_kext_alpha": "Vyhlazující faktor pro nepřetržité učení Kext (Alpha). Výchozí 0,04 (přibližně 3-5 dní adaptace)." + } + } + }, + "error": { + "unknown": "Neočekávaná chyba", + "unknown_entity": "Neznámé ID entity", + "window_open_detection_method": "Měla by se používat pouze jedna metoda detekce otevřeného okna. Použijte buď senzor okna, nebo automatickou detekci pomocí teplotního prahu, ale ne oboje", + "no_central_config": "Nemůžete vybrat 'použít centrální konfiguraci', protože nebyla nalezena žádná centrální konfigurace. Pro její použití musíte vytvořit Versatile Thermostat typu 'Centrální konfigurace'.", + "max_opening_degrees_format": "Seznam celých kladných čísel mezi 0 a 100 oddělených čárkou je očekáván. Příklad: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Pro každé základní zařízení musí být max_opening_degrees striktně větší než min_opening_degrees. Zkontrolujte prosím svou konfiguraci.", + "vswitch_configuration_incorrect": "Konfigurace virtuálního spínače je nesprávná. Jedna nebo více podřízených entit není správně nakonfigurována.", + "service_configuration_format": "Formát konfigurace služby je nesprávný", + "valve_regulation_nb_entities_incorrect": "Počet entit ventilů pro regulaci ventilů by měl být roven počtu podřízených zařízení", + "sync_device_internal_temp_nb_entities_incorrect": "Počet entit pro synchronizaci vnitřní teploty zařízení by se měl rovnat počtu podřízených zařízení", + "min_opening_degrees_format": "Očekává se seznam kladných celých čísel oddělených čárkami. Příklad: 20, 25, 30" + }, + "abort": { + "already_configured": "Zařízení je již nakonfigurováno" + } + }, + "options": { + "flow_title": "Konfigurace Versatile Thermostat", + "step": { + "user": { + "title": "Typ - {name}", + "data": { + "thermostat_type": "Typ termostatu" + }, + "data_description": { + "thermostat_type": "Možný je pouze jeden typ centrální konfigurace" + }, + "description": "Vyberte typ termostatu, který chcete vytvořit" + }, + "menu": { + "title": "Nabídka", + "description": "Nakonfigurujte váš termostat. Konfiguraci budete moci dokončit až po zadání všech požadovaných parametrů.", + "menu_options": { + "main": "Hlavní atributy", + "central_boiler": "Centrální kotel", + "type": "Podřízená zařízení", + "tpi": "TPI parametry", + "features": "Funkce", + "presets": "Předvolby", + "window": "Detekce oken", + "motion": "Detekce pohybu", + "power": "Správa výkonu", + "presence": "Detekce přítomnosti", + "advanced": "Pokročilé parametry", + "auto_start_stop": "Automatické spuštění a zastavení", + "valve_regulation": "Konfigurace regulace ventilu", + "sync_device_internal_temp": "Synchronizace teploty zařízení", + "heating_failure_detection": "Detekce poruchy vytápění", + "lock": "Zámek", + "finalize": "Vše hotovo", + "configuration_not_complete": "Konfigurace není kompletní" + } + }, + "lock": { + "title": "Správa zamknutí - {name}", + "description": "Funkce zamknutí zabraňuje změnám termostatu z uživatelského rozhraní nebo automatizací, přičemž termostat zůstává funkční.", + "data": { + "use_lock_central_config": "Použít centrální konfiguraci zamknutí", + "lock_users": "Zamknutí zabrání příkazům uživatelů", + "lock_automations": "Zamknutí zabrání příkazům z automatizací a integrací (např. plánovač)", + "auto_relock_sec": "Automatické opětovné zamknutí", + "lock_code": "Volitelný PIN kód (4 číslice)" + }, + "data_description": { + "use_lock_central_config": "Zaškrtněte pro použití centrální konfigurace zamknutí. Odškrtněte pro použití konfigurace zamknutí specifické pro tento VTherm", + "auto_relock_sec": "Prodleva v sekundách před automatickým opětovným zamknutím po odemknutí. Nastavte 0 pro deaktivaci. Výchozí hodnota je 30 sekund." + } + }, + "main": { + "title": "Hlavní - {name}", + "description": "Hlavní povinné atributy [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/base-attributes.md)", + "data": { + "name": "Název", + "thermostat_type": "Typ termostatu", + "temperature_sensor_entity_id": "Teplota místnosti", + "last_seen_temperature_sensor_entity_id": "Čas posledního měření teploty místnosti", + "external_temperature_sensor_entity_id": "ID entity senzoru venkovní teploty", + "cycle_min": "Délka cyklu (minuty)", + "temp_min": "Minimální povolená teplota", + "temp_max": "Maximální povolená teplota", + "step_temperature": "Krok teploty", + "device_power": "Výkon zařízení", + "use_central_mode": "Povolit řízení centrální entitou (vyžaduje centrální konfiguraci). Zaškrtněte pro povolení řízení VTherm pomocí entit select central_mode.", + "use_main_central_config": "Použít dodatečnou centrální hlavní konfiguraci. Zaškrtněte pro použití centrální hlavní konfigurace (venkovní teplota, min, max, krok, ...).", + "used_by_controls_central_boiler": "Používán centrálním kotlem. Zaškrtněte, pokud má tento VTherm řídit centrální kotel" + }, + "data_description": { + "cycle_min": "Minimální délka jednoho cyklu vytápění/chlazení v minutách, aby se zabránilo příliš častému přepínání", + "temp_min": "Minimální povolená teplota pro termostat", + "temp_max": "Maximální povolená teplota pro termostat", + "step_temperature": "Krok teploty pro nastavení cílové teploty", + "temperature_sensor_entity_id": "ID entity senzoru teploty místnosti", + "last_seen_temperature_sensor_entity_id": "ID entity posledního času měření teploty místnosti. Měl by být datetime senzor", + "external_temperature_sensor_entity_id": "ID entity senzoru venkovní teploty. Nepoužívá se, pokud je zvolena centrální konfigurace" + } + }, + "features": { + "title": "Funkce - {name}", + "description": "Funkce termostatu", + "data": { + "use_window_feature": "Použít detekci oken", + "use_motion_feature": "Použít detekci pohybu", + "use_power_feature": "Použít správu výkonu", + "use_presence_feature": "Použít detekci přítomnosti", + "use_central_boiler_feature": "Použít centrální kotel. Zaškrtněte pro přidání ovládání centrálního kotle. Po zaškrtnutí budete muset nakonfigurovat VTherm, který bude mít kontrolu nad centrálním kotlem. Pokud jeden VTherm vyžaduje vytápění, kotel se zapne. Pokud žádný VTherm nevyžaduje vytápění, kotel se vypne. Příkazy pro zapnutí/vypnutí centrálního kotle jsou uvedeny na související konfigurační stránce", + "use_auto_start_stop_feature": "Použít funkci automatického spuštění a zastavení", + "use_heating_failure_detection_feature": "Použít detekci poruchy vytápění" + } + }, + "type": { + "title": "Propojené entity - {name}", + "description": "Atributy propojených entit [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/over-switch.md#konfigurace)", + "data": { + "underlying_entity_ids": "Zařízení, která mají být řízena", + "heater_keep_alive": "Interval keep-alive spínače v sekundách", + "proportional_function": "Algoritmus", + "ac_mode": "AC režim", + "sync_device_internal_temp": "Synchronizovat vnitřní teplotu zařízení", + "auto_regulation_mode": "Samoregulace", + "auto_regulation_dtemp": "Práh regulace", + "auto_regulation_periode_min": "Minimální perioda regulace", + "auto_regulation_use_device_temp": "Použít vnitřní teplotu podřízeného zařízení", + "inverse_switch_command": "Invertovat příkaz spínače", + "auto_fan_mode": "Automatický režim ventilátoru", + "on_command_text": "Přizpůsobení příkazu zapnutí", + "vswitch_on_command": "Volitelné příkazy zapnutí", + "off_command_text": "Přizpůsobení příkazu vypnutí", + "vswitch_off_command": "Volitelné příkazy vypnutí" + }, + "data_description": { + "underlying_entity_ids": "Zařízení, která mají být řízena - 1 je povinné", + "heater_keep_alive": "Volitelný interval obnovení stavu spínače vytápění. Ponechte prázdné, pokud není potřeba.", + "proportional_function": "Algoritmus k použití (další algoritmy lze nainstalovat jako {plugins_link})", + "ac_mode": "Použít režim klimatizace (AC)", + "sync_device_internal_temp": "Synchronizovat vnitřní teplotu podřízených zařízení s vybranými entitami", + "auto_regulation_mode": "Automatické přizpůsobení cílové teploty", + "auto_regulation_dtemp": "Práh v ° (nebo % pro ventil), pod kterým nebude změna teploty odeslána", + "auto_regulation_periode_min": "Doba v minutách mezi dvěma aktualizacemi regulace", + "auto_regulation_use_device_temp": "Použít případný vnitřní teplotní senzor podřízeného zařízení pro zrychlení samoregulace", + "inverse_switch_command": "Pro spínače s pilotem a diodou možná budete muset invertovat příkaz", + "auto_fan_mode": "Automaticky aktivovat ventilátor při nutnosti velkého vytápění/chlazení", + "on_command_text": "Pro podřízená zařízení typu `select` nebo `climate` musíte přizpůsobit příkazy." + } + }, + "tpi": { + "title": "TPI – {name}", + "description": "Atributy Time Proportional Integral [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/algorithms.md#algoritmus-tpi)", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "use_tpi_central_config": "Použít centrální TPI konfiguraci", + "tpi_threshold_low": "Nízký práh TPI", + "tpi_threshold_high": "Vysoký práh TPI", + "minimal_activation_delay": "Minimální zpoždění aktivace", + "minimal_deactivation_delay": "Minimální zpoždění deaktivace", + "auto_tpi_mode": "Povolit automatické učení TPI" + }, + "data_description": { + "tpi_coef_int": "Koeficient pro delta vnitřní teploty", + "tpi_coef_ext": "Koeficient pro delta vnější teploty", + "use_tpi_central_config": "Zaškrtněte pro použití centrální TPI konfigurace. Odšktněte pro použití specifické TPI konfigurace pro tento VTherm", + "tpi_threshold_low": "Práh ve °, pod kterým bude algoritmus TPI zapnutý. 0 znamená žádný práh", + "tpi_threshold_high": "Práh ve °, nad kterým bude algoritmus TPI vypnutý. 0 znamená žádný práh", + "minimal_activation_delay": "Zpoždění v sekundách, při kterém se zařízení neaktivuje", + "minimal_deactivation_delay": "Prodleva v sekundách, po kterou bude zařízení udržováno aktivní", + "auto_tpi_mode": "Učení TPI se spouští ručně, nejprve si přečtěte dokument." + } + }, + "presets": { + "title": "Předvolby - {name}", + "description": "Zaškrtněte, pokud bude termostat používat centrální předvolby. Odškrtněte a termostat bude mít vlastní entity předvoleb [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-presets.md)", + "data": { + "use_presets_central_config": "Použít centrální konfiguraci předvoleb" + } + }, + "window": { + "title": "Okna - {name}", + "description": "Správa otevřených oken.\nMůžete také nakonfigurovat automatickou detekci otevřených oken založenou na poklesu teploty [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-window.md)", + "data": { + "window_sensor_entity_id": "ID entity senzoru okna", + "window_delay": "Zpoždění 'zapnuto' senzoru okna (sekundy)", + "window_off_delay": "Zpoždění 'vypnuto' senzoru okna (sekundy)", + "window_auto_open_threshold": "Práh poklesu teploty pro automatickou detekci otevřeného okna (v °/hodina)", + "window_auto_close_threshold": "Práh zvýšení teploty pro ukončení automatické detekce (v °/hodina)", + "window_auto_max_duration": "Maximální doba automatické detekce otevřeného okna (v min)", + "use_window_central_config": "Použít centrální konfiguraci oken", + "window_action": "Akce" + }, + "data_description": { + "window_sensor_entity_id": "Ponechte prázdné, pokud se nemá používat senzor okna a má se používat automatická detekce", + "window_delay": "Zpoždění v sekundách před zohledněním detekce 'zapnuto' senzoru", + "window_off_delay": "Zpoždění v sekundách před zohledněním detekce 'vypnuto' senzoru. Ponechte prázdné pro použití stejné hodnoty jako zpoždění zapnutí okna", + "window_auto_close_threshold": "Doporučená hodnota: 0. Ponechte prázdné, pokud se automatická detekce otevřeného okna nepoužívá", + "window_auto_max_duration": "Doporučená hodnota: 60 (jedna hodina). Ponechte prázdné, pokud se automatická detekce otevřeného okna nepoužívá", + "use_window_central_config": "Zaškrtněte pro použití centrální konfigurace oken. Odšktněte pro použití specifické konfigurace oken pro tento VTherm", + "window_action": "Akce, která se má provést při detekci otevřeného okna", + "window_auto_open_threshold": "Doporučená hodnota: mezi 3 a 10. Pokud není použita automatická detekce otevření okna, ponechte prázdné" + } + }, + "motion": { + "title": "Pohyb - {name}", + "description": "Správa pohybu. Předvolba se může automaticky přepínat v závislosti na detekci pohybu\npředvolba při pohybu (motion_preset) a předvolba bez pohybu (no_motion_preset) by měly být nastaveny na odpovídající název předvolby [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-motion.md)", + "data": { + "motion_sensor_entity_id": "ID entity senzoru pohybu", + "motion_delay": "Zpoždění aktivace", + "motion_off_delay": "Zpoždění deaktivace", + "motion_preset": "Předvolba pohybu", + "no_motion_preset": "Předvolba bez pohybu", + "use_motion_central_config": "Použít centrální konfiguraci pohybu" + }, + "data_description": { + "motion_sensor_entity_id": "ID entity senzoru pohybu", + "motion_delay": "Zpoždění aktivace pohybu (sekundy)", + "motion_off_delay": "Zpoždění deaktivace pohybu (sekundy)", + "motion_preset": "Předvolba k použití při detekci pohybu", + "no_motion_preset": "Předvolba k použití při nedetekci pohybu", + "use_motion_central_config": "Zaškrtněte pro použití centrální konfigurace pohybu. Odškrtněte pro použití specifické konfigurace pohybu pro tento VTherm" + } + }, + "power": { + "title": "Výkon - {name}", + "description": "Atributy správy výkonu.\nUdává výkon a maximální výkonový senzor vašeho domova.\nUveďte spotřebu energie ohřívače při zapnutí.\nVšechny senzory a výkon zařízení by měly používat stejnou jednotku (kW nebo W) [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-power.md)", + "data": { + "power_sensor_entity_id": "Výkon", + "max_power_sensor_entity_id": "Maximální výkon", + "power_temp": "Teplota odlehčení", + "use_power_central_config": "Použít centrální konfiguraci výkonu" + }, + "data_description": { + "power_sensor_entity_id": "ID entity senzoru výkonu", + "max_power_sensor_entity_id": "ID entity senzoru maximálního výkonu", + "power_temp": "Teplota pro odlehčení výkonu", + "use_power_central_config": "Zaškrtněte pro použití centrální konfigurace výkonu. Odškrtněte pro použití specifické konfigurace výkonu pro tento VTherm" + } + }, + "presence": { + "title": "Přítomnost - {name}", + "description": "Atributy správy přítomnosti.\nUdává senzor přítomnosti vašeho domova (true znamená, že je někdo přítomen) a dává odpovídající nastavení teplotní předvolby [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-presence.md)", + "data": { + "presence_sensor_entity_id": "Senzor přítomnosti", + "use_presence_central_config": "Použít centrální konfiguraci teploty přítomnosti. Odškrtněte pro použití specifických teplotních entit" + }, + "data_description": { + "presence_sensor_entity_id": "ID entity senzoru přítomnosti" + } + }, + "advanced": { + "title": "Pokročilé - {name}", + "description": "Konfigurace pokročilých parametrů. Pokud nevíte, co děláte, ponechte výchozí hodnoty.\nTyto parametry mohou vést k velmi špatné regulaci teploty nebo špatné regulaci výkonu [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-advanced.md)", + "data": { + "minimal_activation_delay": "Minimální zpoždění aktivace", + "minimal_deactivation_delay": "Minimální zpoždění deaktivace", + "safety_delay_min": "Bezpečnostní zpoždění (v minutách)", + "safety_min_on_percent": "Minimální procento výkonu pro povolení bezpečnostního režimu", + "safety_default_on_percent": "Procento výkonu k použití v bezpečnostním režimu", + "repair_incorrect_state": "Opravit nesprávný stav", + "use_advanced_central_config": "Použít centrální pokročilou konfiguraci" + }, + "data_description": { + "minimal_activation_delay": "Zpoždění v sekundách, pod kterým nebude zařízení aktivováno", + "minimal_deactivation_delay": "Zpoždění v sekundách, po kterou bude zařízení udržováno aktivní", + "safety_delay_min": "Maximální povolené zpoždění v minutách mezi dvěma měřeními teploty. Nad tímto zpožděním se termostat přepne do bezpečnostního vypnutého stavu", + "safety_min_on_percent": "Minimální hodnota procenta vytápění pro aktivaci bezpečnostní předvolby. Pod tímto množstvím procent výkonu se termostat nepřepne do bezpečnostní předvolby", + "safety_default_on_percent": "Výchozí hodnota procenta výkonu vytápění v bezpečnostní předvolbě. Nastavte na 0 pro vypnutí ohřívače v bezpečnostní předvolbě", + "repair_incorrect_state": "Automaticky opravit nesprávný stav základních entit při zjištění. Funkce znovu pošle požadovaný příkaz, pokud stav základní entity neodpovídá očekávanému stavu.", + "use_advanced_central_config": "Zaškrtněte pro použití centrální pokročilé konfigurace. Odškrtněte pro použití specifické pokročilé konfigurace pro tento VTherm" + } + }, + "central_boiler": { + "title": "Ovládání centrálního kotle - {name}", + "description": "Zadejte služby, které se mají volat pro zapnutí/vypnutí centrálního kotle. Volaná služba musí být naformátována následovně: `entity_id/service_name[/attribute:value]` (/attribute:value je volitelné)\nNapříklad:\n- pro zapnutí spínače: `switch.controle_chaudiere/switch.turn_on`\n- pro vypnutí spínače: `switch.controle_chaudiere/switch.turn_off`\n- pro naprogramování kotle na 25° a tím vynucení jeho zapálení: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- pro poslání 10° do kotle a tím vynucení jeho vypnutí: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10` [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/feature-central-boiler.md)", + "data": { + "central_boiler_activation_service": "Příkaz pro zapnutí", + "central_boiler_deactivation_service": "Příkaz pro vypnutí", + "central_boiler_activation_delay_sec": "Zpoždění aktivace (v sekundách)", + "keep_alive_boiler_delay_sec": "Zpoždění keep-alive (sekundy)" + }, + "data_description": { + "central_boiler_activation_service": "Příkaz pro zapnutí centrálního kotle naformátovaný jako entity_id/service_name[/attribut:hodnota]", + "central_boiler_deactivation_service": "Příkaz pro vypnutí centrálního kotle naformátovaný jako entity_id/service_name[/attribut:hodnota]", + "central_boiler_activation_delay_sec": "Zpoždění aktivace (v sekundách) před zapnutím centrálního kotle, když je potřeba jej zapnout", + "keep_alive_boiler_delay_sec": "Zpoždění v sekundách pro periodické opakování příkazu kotle, pokud je keep-alive povoleno. Nastavte na 0, aby se keep-alive zakázal." + } + }, + "valve_regulation": { + "title": "Samoregulace s ventilem - {name}", + "description": "Konfigurace pro samoregulaci s přímým ovládáním ventilu [![?](https://img.icons8.com/color/18/help.png)](https://github.com/jmcollin78/versatile_thermostat/blob/main/documentation/cs/self-regulation.md#samo-regulace-přímým-ovládáním-ventilu)", + "data": { + "offset_calibration_entity_ids": "Entita kalibrace offsetu", + "opening_degree_entity_ids": "Entita stupně otevření", + "closing_degree_entity_ids": "Entita stupně zavření", + "proportional_function": "Algoritmus", + "opening_threshold_degree": "Prahová hodnota otevření", + "min_opening_degrees": "Minimální stupně otevření", + "max_opening_degrees": "Maximální stupeň otevření", + "max_closing_degree": "Maximální stupeň zavření" + }, + "data_description": { + "offset_calibration_entity_ids": "Seznam entit 'kalibrace offsetu'. Nastavte, pokud má váš TRV entitu pro lepší regulaci. Měla by být jedna na každou podřízenou klimatizační entitu", + "opening_degree_entity_ids": "Seznam entit 'stupně otevření'. Měla by být jedna na každou podřízenou klimatizační entitu", + "closing_degree_entity_ids": "Seznam entit 'stupně zavření'. Nastavte, pokud má váš TRV entitu pro lepší regulaci. Měla by být jedna na každou podřízenou klimatizační entitu", + "proportional_function": "Algoritmus k použití (další algoritmy lze nainstalovat jako {plugins_link})", + "opening_threshold_degree": "Hodnota otevření ventilu, pod kterou by měl být ventil považován za zavřený (a pak bude aplikován 'max_closing_degree')", + "min_opening_degrees": "Minimální hodnota stupně otevření pro každé podřízené zařízení, oddělené čárkami. Výchozí je 0. Příklad: 20, 25, 30", + "max_opening_degrees": "Maximální hodnota stupně otevření pro každé základní zařízení, oddělené čárkami. Výchozí hodnota je 100. Ventil se nikdy neotevře více než tato hodnota. Příklad: 80, 85, 90", + "max_closing_degree": "Maximální hodnota stupně zavření. Ventil nebude nikdy uzavřen více než tato hodnota. Nastavte na 100 pro úplné zavření ventilu" + } + }, + "sync_device_internal_temp": { + "title": "Synchronizujte vnitřní teplotu zařízení", + "description": "Konfigurace pro synchronizaci vnitřní teploty základních zařízení s vybranými entitami", + "data": { + "sync_with_calibration": "Použít kalibraci offsetu", + "sync_entity_ids": "ID entity použité pro synchronizaci" + }, + "data_description": { + "sync_with_calibration": "Zaškrtněte, chcete-li použít kalibraci offsetu při synchronizaci vnitřní teploty. Zrušte zaškrtnutí, chcete-li přímo zkopírovat teplotu z vybraných entit", + "sync_entity_ids": "Seznam entit používaných k synchronizaci vnitřní teploty základních zařízení. Na každé základní zařízení by měl být jeden. Pokud je zaškrtnuto políčko, měla by to být kalibrační entita nebo jinak entita teploty. Oba jsou entity typu „číslo“." + } + }, + "heating_failure_detection": { + "title": "Detekce poruchy vytápění - {name}", + "description": "Nakonfigurujte funkci detekce poruchy vytápění. To detekuje anomálie, když se očekává ohřev, ale teplota se nezvyšuje, nebo když je topení vypnuté, ale teplota stále stoupá.", + "data": { + "use_heating_failure_detection_feature": "Povolit detekci poruchy vytápění", + "use_heating_failure_detection_central_config": "Použít centrální konfiguraci detekce poruchy vytápění", + "heating_failure_threshold": "Prahová hodnota poruchy vytápění", + "cooling_failure_threshold": "Prahová hodnota poruchy chlazení", + "heating_failure_detection_delay": "Zpoždění detekce (minuty)", + "temperature_change_tolerance": "Tolerance změny teploty (°C)", + "failure_detection_enable_template": "Šablona povolení detekce" + }, + "data_description": { + "use_heating_failure_detection_feature": "Povolte detekci anomálií ohřevu pro VTherm s TPI", + "use_heating_failure_detection_central_config": "Zaškrtněte pro použití centrální konfigurace detekce poruchy vytápění. Odškrtněte pro použití specifické konfigurace pro tento VTherm", + "heating_failure_threshold": "Prahová hodnota on_percent nad kterou by mělo zahřívání způsobit zvýšení teploty (0,9 = 90 %)", + "cooling_failure_threshold": "Prahová hodnota on_percent pod kterou by se teplota neměla zvýšit (0,0 = 0 %)", + "heating_failure_detection_delay": "Doba v minutách před zkontrolováním, zda se teplota změnila podle očekávání", + "temperature_change_tolerance": "Minimální změna teploty ve stupních, kterou je třeba považovat za významnou. Menší změny jsou ignorovány pro filtrování šumu senzoru (výchozí: 0,5 °C)", + "failure_detection_enable_template": "An optional Jinja2 template that must return True to enable detection. Useful to temporarily disable detection when an external heat source is active (e.g., is_state('binary_sensor.wood_stove', 'off') in double curly braces). If empty, detection is always enabled." + } + }, + "auto_tpi_configuration": { + "title": "Automatické TPI – Konfigurace – {name}", + "description": "Obecná nastavení pro automatické učení TPI.", + "data": { + "auto_tpi_learning_type": "Typ učení", + "auto_tpi_heating_rate": "Rychlost ohřevu ({unit}/h)", + "heater_heating_time": "Doba ohřevu (min)", + "heater_cooling_time": "Doba chlazení (min)", + "auto_tpi_aggressiveness": "Agresivita", + "auto_tpi_enable_advanced_settings": "Povolit pokročilé parametry", + "auto_tpi_continuous_kext": "Průběžné učení Kext" + }, + "data_description": { + "auto_tpi_learning_type": "Zvolte 'Discovery' pro počáteční spuštění (vážený průměr, váha 1) nebo 'Fine Tuning' pro průběžné úpravy (EWMA, Alpha 0,08).", + "auto_tpi_heating_rate": "Kapacita nárůstu teploty radiátoru ({unit} za hodinu). Ponechejte na 0 pro automatické učení (bootstrap).", + "heater_heating_time": "Čas k dosažení plného výkonu (minuty)", + "heater_cooling_time": "Čas na ochlazení po zastavení (minuty)\n\n| Typ | Doba ohřevu | Doba chlazení |\n| :--- | :--- | :--- |\n| Elektrický radiátor | 5 min | 7mn |\n| Vodní radiátor | 15 minut | 20 min |\n| Podlahové vytápění | 30 min | 45 min |", + "auto_tpi_aggressiveness": "Redukční faktor pro naučené koeficienty (50-100%). Nižší hodnoty vedou ke konzervativnějším koeficientům a snižují riziko přestřelení.", + "auto_tpi_enable_advanced_settings": "Zaškrtnutím změníte parametry zvoleného algoritmu.", + "auto_tpi_continuous_kext": "Zaškrtnutím povolíte průběžné učení externího koeficientu (Kext) mimo relace AutoTPI." + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI – vážený průměr – {name}", + "description": "Parametry pro metodu váženého průměru.", + "data": { + "auto_tpi_avg_initial_weight": "Počáteční hmotnost" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Počáteční váha existujících koeficientů ve výpočtu průměru (typicky 1-50). Vyšší váha znamená pomalejší a stabilnější učení." + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI – EWMA – {name}", + "description": "Parametry pro metodu EWMA.\n\nDoporučení:\n| Situace | Alfa (ema_alpha) | Decay Rate (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Silné úpravy | 0,15 | 0,08 |\n| Jemné doladění | 0,08 | 0,12 |", + "data": { + "auto_tpi_ema_alpha": "Alfa", + "auto_tpi_ema_decay_rate": "Míra rozpadu", + "auto_tpi_continuous_kext_alpha": "Kontinuální Kext Alpha" + }, + "data_description": { + "auto_tpi_ema_alpha": "Faktor vyhlazování (0-1). Vyšší = rychlejší adaptace", + "auto_tpi_ema_decay_rate": "Rychlost, se kterou alfa klesá v průběhu času (stabilizace)", + "auto_tpi_continuous_kext_alpha": "Vyhlazující faktor pro nepřetržité učení Kext (Alpha). Výchozí 0,04 (přibližně 3-5 dní adaptace)." + } + } + }, + "error": { + "unknown": "Neočekávaná chyba", + "unknown_entity": "Neznámé ID entity", + "window_open_detection_method": "Měla by se používat pouze jedna metoda detekce otevřeného okna. Použijte buď senzor okna, nebo automatickou detekci pomocí teplotního prahu, ale ne oboje", + "no_central_config": "Nemůžete zaškrtnout 'použít centrální konfiguraci', protože nebyla nalezena žádná centrální konfigurace. Pro její použití musíte vytvořit Versatile Thermostat typu 'Centrální konfigurace'.", + "max_opening_degrees_format": "Seznam celých kladných čísel mezi 0 a 100 oddělených čárkou je očekáván. Příklad: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Pro každé základní zařízení musí být max_opening_degrees striktně větší než min_opening_degrees. Zkontrolujte prosím svou konfiguraci.", + "vswitch_configuration_incorrect": "Konfigurace virtuálního spínače je nesprávná. Jedna nebo více podřízených entit není správně nakonfigurována.", + "service_configuration_format": "Formát konfigurace služby je nesprávný", + "valve_regulation_nb_entities_incorrect": "Počet entit ventilů pro regulaci ventilů by měl být roven počtu podřízených zařízení", + "sync_device_internal_temp_nb_entities_incorrect": "Počet entit pro synchronizaci vnitřní teploty zařízení by se měl rovnat počtu podřízených zařízení", + "min_opening_degrees_format": "Očekává se seznam kladných celých čísel oddělených čárkami. Příklad: 20, 25, 30" + }, + "abort": { + "already_configured": "Zařízení je již nakonfigurováno" + } + }, + "selector": { + "thermostat_type": { + "options": { + "thermostat_central_config": "Centrální konfigurace", + "thermostat_over_switch": "Termostat přes spínač", + "thermostat_over_climate": "Termostat přes klimatizaci", + "thermostat_over_valve": "Termostat přes ventil" + } + }, + "auto_regulation_mode": { + "options": { + "auto_regulation_slow": "Pomalá", + "auto_regulation_strong": "Silná", + "auto_regulation_medium": "Střední", + "auto_regulation_light": "Lehká", + "auto_regulation_expert": "Expertní", + "auto_regulation_none": "Žádná autoregulace", + "auto_regulation_valve": "Přímé ovládání ventilu" + } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "Žádný automatický ventilátor", + "auto_fan_low": "Nízká", + "auto_fan_medium": "Střední", + "auto_fan_high": "Vysoká", + "auto_fan_turbo": "Turbo režim" + } + }, + "window_action": { + "options": { + "window_turn_off": "Vypnout", + "window_fan_only": "Pouze ventilátor", + "window_frost_temp": "Ochrana proti mrazu", + "window_eco_temp": "Pouze teplota Eco" + } + }, + "presets": { + "options": { + "frost": "Ochrana proti mrazu", + "eco": "Eco", + "comfort": "Komfort", + "boost": "Boost" + } + }, + "auto_start_stop": { + "options": { + "auto_start_stop_none": "Žádné automatické spuštění/zastavení", + "auto_start_stop_very_slow": "Velmi pomalá detekce", + "auto_start_stop_slow": "Pomalá detekce", + "auto_start_stop_medium": "Střední detekce", + "auto_start_stop_fast": "Rychlá detekce" + } + }, + "auto_tpi_calculation_method": { + "options": { + "average": "Průměrný", + "ema": "Exponenciální klouzavý průměr (EMA)" + } + }, + "auto_tpi_learning_type": { + "options": { + "discovery": "Objev", + "fine_tuning": "Jemné ladění" + } + } + }, + "entity": { + "climate": { + "versatile_thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "power": "Odlehčení", + "safety": "Bezpečnost", + "none": "Manuální", + "frost": "Mráz" + } + } + } + } + }, + "number": { + "frost_temp": { + "name": "Mráz" + }, + "eco_temp": { + "name": "Úsporná" + }, + "comfort_temp": { + "name": "Komfort" + }, + "boost_temp": { + "name": "Boost" + }, + "frost_ac_temp": { + "name": "Mráz AC" + }, + "eco_ac_temp": { + "name": "Eco AC" + }, + "comfort_ac_temp": { + "name": "Komfort AC" + }, + "boost_ac_temp": { + "name": "Boost AC" + }, + "frost_away_temp": { + "name": "Mráz mimo domov" + }, + "eco_away_temp": { + "name": "Eco mimo domov" + }, + "comfort_away_temp": { + "name": "Komfort mimo domov" + }, + "boost_away_temp": { + "name": "Boost mimo domov" + }, + "eco_ac_away_temp": { + "name": "Eco AC mimo domov" + }, + "comfort_ac_away_temp": { + "name": "Komfort AC mimo domov" + }, + "boost_ac_away_temp": { + "name": "Boost AC mimo domov" + } + }, + "binary_sensor": { + "safety_state": { + "name": "Bezpečnostní stav" + }, + "heating_failure_state": { + "name": "Stav poruchy vytápění" + }, + "overpowering_state": { + "name": "Stav přetížení" + }, + "window_state": { + "name": "Stav okna" + }, + "motion_state": { + "name": "Stav pohybu" + }, + "presence_state": { + "name": "Stav přítomnosti" + }, + "window_bypass_state": { + "name": "Okenní bypass" + }, + "central_boiler_state": { + "name": "Centrální kotel" + } + } + }, + "services": { + "set_auto_tpi_mode": { + "name": "Nastavte režim Auto TPI", + "description": "Povolte nebo zakažte režim automatického učení TPI", + "fields": { + "auto_tpi_mode": { + "name": "Režim Auto TPI", + "description": "Povolte (pravda) nebo zakažte (nepravda) automatické učení TPI" + }, + "reinitialise": { + "name": "Resetovat data učení", + "description": "Při aktivaci režimu Auto TPI resetujte všechna data učení (výchozí hodnota je true)" + } + } + }, + "reset_auto_tpi_capacities": { + "name": "Resetujte automatické kapacity TPI", + "description": "Resetujte naučené maximální topné a chladicí výkony na 1,0 °C/h. Naučené koeficienty NENÍ resetováno." + }, + "auto_tpi_calibrate_capacity": { + "name": "Automatická kalibrace kapacity TPI", + "description": "Kalibrujte výkon vytápění/chlazení (ve °C/hodinu) pomocí lineární regrese na historických datech entity. Naučené koeficienty NEJSOU aktualizovány.", + "fields": { + "start_date": { + "name": "Historie od data", + "description": "Datum, od kterého se má načíst historie (výchozí je před 30 dny)" + }, + "end_date": { + "name": "Historie k dnešnímu dni", + "description": "Datum, do kterého se má načíst historie (výchozí je nyní)" + }, + "hvac_mode": { + "name": "Režim HVAC", + "description": "Jaký režim kalibrovat (topení nebo chlazení)." + }, + "save_to_config": { + "name": "Uložit do konfigurace", + "description": "Použijte vypočítanou kapacitu na konfiguraci Auto TPI." + }, + "min_power_threshold": { + "name": "Minimální práh výkonu", + "description": "Minimální procento výkonu, aby byl cyklus považován za nasycený (výchozí 95 %). Nižší hodnoty budou zahrnovat více cyklů, ale mohou být méně přesné." + } + } + }, + "set_tpi_parameters": { + "name": "Nastavte parametry TPI", + "description": "Změňte parametry TPI", + "fields": { + "minimal_activation_delay": { + "name": "Minimální zpoždění aktivace", + "description": "Zpoždění v sekundách, při kterém se zařízení neaktivuje" + }, + "minimal_deactivation_delay": { + "name": "Minimální zpoždění deaktivace", + "description": "Prodleva v sekundách, po kterou bude zařízení udržováno aktivní" + }, + "tpi_threshold_low": { + "name": "Nízký práh TPI", + "description": "Práh ve °, pod kterým bude algoritmus TPI zapnutý. 0 znamená žádný práh" + }, + "tpi_threshold_high": { + "name": "Vysoký práh TPI", + "description": "Práh ve °, nad kterým bude algoritmus TPI vypnutý. 0 znamená žádný práh" + } + } + }, + "lock": { + "name": "Zámek", + "description": "Uzamkne termostat a zabrání jakýmkoli změnám jeho konfigurace z uživatelského rozhraní nebo automatizace.", + "fields": { + "code": { + "name": "Kód zámku", + "description": "Volitelný zamykací kód" + } + } + }, + "unlock": { + "name": "Odemknout", + "description": "Odemyká termostat a umožňuje změny jeho konfigurace.", + "fields": { + "code": { + "name": "Kód zámku", + "description": "Volitelný zamykací kód" + } + } + }, + "download_logs": { + "name": "Stáhnout protokoly", + "description": "Sbírejte a stahujte filtrované protokoly pro entitu VTherm.", + "fields": { + "log_level": { + "name": "Úroveň protokolu", + "description": "Minimální úroveň protokolu k zahrnutí" + }, + "period_start": { + "name": "Začátek období", + "description": "Začátek doby těžby" + }, + "period_end": { + "name": "Konec období", + "description": "Konec doby těžby" + } + } + } + }, + "exceptions": { + "auto_tpi_learning_stopped": { + "message": "Automatické učení TPI pro {name} bylo zastaveno kvůli 3 po sobě jdoucím selháním. Důvod: {reason}. Zkontrolujte prosím svou konfiguraci." + } + } +} diff --git a/custom_components/versatile_thermostat/translations/de.json b/custom_components/versatile_thermostat/translations/de.json new file mode 100644 index 0000000..04b6b89 --- /dev/null +++ b/custom_components/versatile_thermostat/translations/de.json @@ -0,0 +1,1023 @@ +{ + "title": "Versatile Thermostat", + "config": { + "flow_title": "Versatile Thermostat Konfiguration", + "step": { + "user": { + "title": "Typ des Versatile Thermostat", + "description": "Zu erstellenden Thermostattyp auswählen.", + "data": { + "thermostat_type": "Thermostattyp" + }, + "data_description": { + "thermostat_type": "Es ist nur ein zentraler Konfigurationstyp möglich" + } + }, + "menu": { + "title": "Menü", + "description": "Konfigurieren Sie Ihr Thermostat. Sobald alle erforderlichen Parameter eingegeben sind, kann die Konfiguration abgeschlossen werden.", + "menu_options": { + "main": "Wichtigste Eigenschaften", + "central_boiler": "Zentralheizung", + "type": "Basiswerte", + "tpi": "TPI-Parameter (Time Proportional Integral)", + "features": "Funktionen", + "presets": "Temperaturvoreinstellungen", + "window": "Fensteröffnugserkennung", + "motion": "Bewegungserkennung", + "power": "Powermanagement", + "presence": "Anwesenheitserkennung", + "advanced": "Erweiterte Einstellungen", + "auto_start_stop": "Auto Start und Stop", + "valve_regulation": "Konfiguration Ventilsteuerung", + "sync_device_internal_temp": "Synchronisierung der Gerätetemperatur", + "lock": "Sperrenverwaltung", + "heating_failure_detection": "Heizungsausfallerkennung", + "auto_tpi_configuration": "Auto TPI - Konfiguration", + "auto_tpi_avg_settings": "Auto TPI - Durchschnitt", + "auto_tpi_ema_settings": "Auto TPI - EMA", + "finalize": "Alles erledigt", + "configuration_not_complete": "Konfiguration unvollständig" + } + }, + "lock": { + "title": "Verwaltung der Sperre", + "description": "Die Sperrfunktion verhindert Änderungen an einem Thermostat über die Benutzeroberfläche oder Automatisierungen, während der Thermostat weiterhin funktioniert.", + "data": { + "use_lock_central_config": "Zentrale Sperrkonfiguration verwenden", + "lock_code": "Optionaler PIN-Code (4 Ziffern)", + "lock_users": "Sperre verhindert Benutzerbefehle", + "lock_automations": "Sperre verhindert Befehle von Automatisierungen und Integrationen (z.B. Planer)", + "auto_relock_sec": "Automatische Wiederverriegelung" + }, + "data_description": { + "use_lock_central_config": "Aktivieren für zentrale Sperrkonfiguration. Deaktivieren für sperrspezifische Konfiguration dieses VTherm", + "auto_relock_sec": "Verzögerung in Sekunden bevor nach dem Entsperren automatisch wieder verriegelt wird. 0 zum Deaktivieren. Standardwert ist 30 Sekunden." + } + }, + "main": { + "title": "Neues Versatile Thermostat", + "description": "Wichtigste notwendige Eigenschaften", + "data": { + "name": "Name", + "thermostat_type": "Thermostattyp", + "temperature_sensor_entity_id": "Raumtemperatursensor", + "last_seen_temperature_sensor_entity_id": "Letzte Sichtung des Temperatursensors", + "external_temperature_sensor_entity_id": "Außentemperatursensors", + "cycle_min": "Zyklusdauer (Minuten)", + "temp_min": "Zulässige Mindesttemperatur", + "temp_max": "Zulässige Höchsttemperatur", + "step_temperature": "Temperaturschritte", + "device_power": "Geräteleistung", + "use_central_mode": "Kontrolle durch zentrale Stelle zulassen (zentrale Konfiguration erforderlich). Aktivieren, um die Steuerung des VTherm mit den ausgewählten Zentralmodus-Entities zu aktivieren.", + "use_main_central_config": "Zusätzliche zentrale Konfiguration verwenden. Anwählen, wenn eine zusätzliche zentrale Konfiguration verwendet werden soll (Außentemperatur, Min, Max, Stufe, ...).", + "used_by_controls_central_boiler": "Zentralheizung verwenden. Wenn angewählt, steuert dieses VTherm den zentralen Heizkessel." + }, + "data_description": { + "cycle_min": "Minimale Dauer eines Heiz-/Kühlzyklus in Minuten, um zu häufiges Schalten zu vermeiden", + "temp_min": "Zulässige Mindesttemperatur für das Thermostat", + "temp_max": "Zulässige Höchsttemperatur für das Thermostat", + "step_temperature": "Temperaturschritt für die Einstellung der Zieltemperatur", + "temperature_sensor_entity_id": "Entity-ID des Raumtemperatursensor", + "last_seen_temperature_sensor_entity_id": "Entity-ID des Sensors, der das Datum und die Uhrzeit der letzten Datenübermittlung vom Temperatursensors angibt. Der Status muss im Format Datum Uhrzeit angegeben werden (z. B.: 2024-03-31T17:07:03+00:00).", + "external_temperature_sensor_entity_id": "Außentemperatursensor Entity-ID. Wird nicht verwendet, wenn zentrale Konfiguration gewählt wurde." + } + }, + "features": { + "title": "Funktionen", + "description": "Thermostatfunktionen", + "data": { + "use_window_feature": "Fensteröffnungserkennung", + "use_motion_feature": "Bewegungserkennung", + "use_power_feature": "Powermanagement", + "use_presence_feature": "Anwesenheitserkennung", + "use_central_boiler_feature": "Zentralheizung hinzufügen. Aktivieren Sie diese Option, um eine Steuerung für eine Zentralheizung hinzuzufügen. Anschließend müssen die VTerms konfiguriert werden, die die Zentralheizung steuern. Sonst ist diese Option unwirksam. Sobald mindestens einer der VTherms heizen muss, wird die Zentralheizung aktiviert. Sollte keiner der VTherms heizen müssen, wird sie ausgeschaltet. Die Befehle zum Ein- und Ausschalten der Zentralheizung befinden sich auf der folgenden Konfigurationsseite", + "use_auto_start_stop_feature": "Mit automatischer Start- und Stoppfunktion", + "use_heating_failure_detection_feature": "Heizungsausfallerkennung" + } + }, + "type": { + "title": "Verknüpfte Entities", + "description": "Verknüpfte Entitie-Attribute", + "data": { + "underlying_entity_ids": "Zu steuernde Geräte", + "heater_keep_alive": "Schaltintervall in Sekunden", + "proportional_function": "Algorithmus", + "ac_mode": "AC-Modus", + "sync_device_internal_temp": "Synchronisierung der internen Gerätetemperatur", + "auto_regulation_mode": "Selbstregelung", + "auto_regulation_dtemp": "Regelungsschwelle", + "auto_regulation_periode_min": "Mindestzeitraum Regelung", + "auto_regulation_use_device_temp": "Interne Bezugstemperatur ausgleichen", + "inverse_switch_command": "Umgekehrter Schalterbefehl", + "auto_fan_mode": "Automatischer Lüftermodus", + "on_command_text": "Anpassung Einschaltbefehl", + "vswitch_on_command": "Einschaltbefehl (optional)", + "off_command_text": "Anpassung Ausschaltbefehl", + "vswitch_off_command": "Ausschaltbefehl (optional)" + }, + "data_description": { + "underlying_entity_ids": "Liste der von diesem VTherm gesteuerten Geräte", + "heater_keep_alive": "Aktualisierungsintervall des Schalters in Sekunden. Leer lassen zum Deaktivieren. Nur für Schalter verwenden, wenn erforderlich", + "proportional_function": "Zu verwendender Algorithmus (andere Algorithmen können als {plugins_link} installiert werden)", + "ac_mode": "Klimaanlage-Modus (AC)", + "sync_device_internal_temp": "Die Innentemperatur der verknüpften Geräte mit der von VTherm gemessenen Temperatur synchronisieren", + "auto_regulation_mode": "Selbstregulierung durch VTherm verwenden", + "auto_regulation_dtemp": "Schwellenwert in ° (oder % bei Ventilen), unterhalb dessen die Regelung nicht aktiviert wird", + "auto_regulation_periode_min": "Dauer in Minuten zwischen zwei Regelungsaktualisierungen", + "auto_regulation_use_device_temp": "Geräteinnentemperatur ausgleichen, zur Selbstregulierungsbeschleunigung (in den meisten Fällen nicht erforderlich)", + "inverse_switch_command": "Schaltersteuerung umkehren für Installationen mit Pilotdraht und Diode.", + "auto_fan_mode": "Automatisch Belüftung bei großen Abweichungen", + "on_command_text": "Für verknüpfte Geräte des Typs `select` oder `climate` müssen die Befehle angepasst werden." + } + }, + "tpi": { + "title": "TPI", + "description": "Attribute des zeitproportionalen Integral (Time Proportional Integral)", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "tpi_threshold_low": "TPI unterer Schwellwert", + "tpi_threshold_high": "TPI oberer Schwellwert", + "minimal_activation_delay": "Mindest**aktivierung**sverzögerung", + "minimal_deactivation_delay": "Mindest**deaktivierung**sverzögerung", + "use_tpi_central_config": "Zentrale TPI-Konfiguration verwenden", + "auto_tpi_mode": "Auto-TPI-Lernen aktivieren" + }, + "data_description": { + "tpi_coef_int": "Zu verwendender Faktor für das interne Temperaturdelta", + "tpi_coef_ext": "Zu verwendender Faktor für das externe Temperaturdelta", + "tpi_threshold_low": "Schwellenwert in °, unter dem der TPI aktiviert wird. 0 bedeutet kein Schwellenwert.", + "tpi_threshold_high": "Schwellenwert in °, über dem der TPI-Algorithmus deaktiviert wird. 0 bedeutet kein Schwellenwert.", + "use_tpi_central_config": "Aktivieren für zentrale TPI-Konfiguration. Bei deaktivierter Option, die Attribute eingeben um eine bestimmte TPI-Konfiguration zu verwenden", + "minimal_activation_delay": "Zeit in Sekunden, unterhalb derer das Gerät nicht aktiviert wird", + "minimal_deactivation_delay": "Zeit in Sekunden, nach der das Gerät in den Ruhezustand wechselt", + "auto_tpi_mode": "Automatisches Lernen der TPI-Koeffizienten aktivieren" + } + }, + "presets": { + "title": "Temperaturvoreinstellungen", + "description": "Zentralen Temperaturvoreinstellungen für dieses Thermostat aktivieren. Für eigene Voreinstellungen des Thermostats, diese Option deaktivieren.", + "data": { + "use_presets_central_config": "Zentralen Temperaturvoreinstellungen verwenden" + } + }, + "window": { + "title": "Fensteröffnung verwalten", + "description": "Heizkörper ausschalten, sobald Fenster offen ist. Entity-ID leer lassen, um die automatische Erkennung zu verwenden.", + "data": { + "window_sensor_entity_id": "Öffnungssensor (Entity-ID)", + "window_delay": "Auswertverzögerung beim Öffnen (Sekunden)", + "window_off_delay": "Auswertverzögerung beim Schliessen (Sekunden)", + "window_auto_open_threshold": "Obere Temperaturabfallschwelle für die automatische Erkennung (in °/Stunden)", + "window_auto_close_threshold": "Untere Temperaturabfallschwelle zum Beenden der automatischen Erkennung (in °/Stunden)", + "window_auto_max_duration": "Maximale Dauer einer automatischen Abschaltung (in min)", + "use_window_central_config": "Zentrale Fensteröffnungskonfiguration verwenden", + "window_action": "Aktion" + }, + "data_description": { + "window_sensor_entity_id": "Leer lassen, wenn kein Fenstersensor verhanden ist und/oder die automatische Erkennung genutzt werden soll", + "window_delay": "Verzögerung in Sekunden, bis die Sensorerkennung 'Offen' berücksichtigt wird.", + "window_off_delay": "Verzögerung in Sekunden, bis die Sensorerkennung 'Geschlossen' berücksichtigt wird. Feld leer lassen, um gleiche Verzögerungszeit bei öffnen und schliessen zu verwenden.", + "window_auto_open_threshold": "Empfohlener Wert: zwischen 3 und 10. Leer lassen, wenn die automatische Erkennung von offenen Fenstern nicht verwendet wird", + "window_auto_close_threshold": "Empfohlener Wert: 0. Leer lassen, wenn die automatische Erkennung nicht verwendet wird.", + "window_auto_max_duration": "Empfohlener Wert: 60 (eine Stunde). Leer lassen, wenn die automatische Erkennung nicht verwendet wird", + "use_window_central_config": "Bei aktivieren dieser Option wird die zentrale Fensterkonfiguration verwenden. Abwählen, um eine spezifische Konfiguration für Fensteröffnung bei diesem VTherm zu verwenden", + "window_action": "Auszuführende Aktion, wenn das Fenster als offen erkannt wird" + } + }, + "motion": { + "title": "Verwaltung Bewegungserkennung", + "description": "Die Voreinstellung passt sich automatisch an, wenn eine Bewegung erkannt wird.\n'Voreinstellung bei Bewegung' und 'Voreinstellung ohne Bewegung' müssen mit den zu verwendenden Voreinstellungen ausgewählt werden.", + "data": { + "motion_sensor_entity_id": "Bewegungsmelder", + "motion_delay": "Aktivierungsverzögerung", + "motion_off_delay": "Deaktivierungsverzögerung", + "motion_preset": "Voreinstellung bei Bewegung", + "no_motion_preset": "Voreinstellung ohne Bewegung", + "use_motion_central_config": "Zentrale Bewegungskonfiguration verwenden" + }, + "data_description": { + "motion_sensor_entity_id": "Bewegungsmelder Entity-ID", + "motion_delay": "Verzögerung vor Aktivierung der Bewegungserkennung (Sekunden)", + "motion_off_delay": "Verzögerung bis zur Deaktivierung, wenn keine Bewegung erkannt wird (Sekunden)", + "motion_preset": "Voreingestellung bei Erkennung einer Bewegung", + "no_motion_preset": "Voreingestellung, die verwendet werden soll, wenn keine Bewegung erkannt wird", + "use_motion_central_config": "Aktivieren Sie diese Option, zur Verwendung der zentralen Bewegungskonfiguration. Nicht ankreuzen, um eine spezifische Bewegungskonfiguration für dieses VTherm zu verwenden." + } + }, + "power": { + "title": "Powermanagement", + "description": "Wählt automatisch die Voreinstellung 'power', wenn der Energieverbrauch ein Maximum überschreitet.\nGibt die Entity-IDs der Sensoren an, die die Gesamtleistung und die maximal zulässige Leistung messen.\nGib anschließend die Leistung des Geräts an.\nAlle Sensoren und die Geräteleistung sollten die gleiche Einheit verwenden (kW oder W).", + "data": { + "power_sensor_entity_id": "Gesamtleistungssensor (Entity-ID)", + "max_power_sensor_entity_id": "Sensor für maximale Leistung (Entity-ID)", + "power_temp": "Temperatur bei Lastabwurf", + "use_power_central_config": "Zentrales Powermanagement verwenden" + }, + "data_description": { + "power_sensor_entity_id": "Entity-ID des Gesamtleistungssensors der Wohnung/des Hauses", + "max_power_sensor_entity_id": "Entity-ID des Maximalleistungssensors vor Lastabwurf", + "power_temp": "Zieltemperatur bei Leistungsreduzierung", + "use_power_central_config": "Aktivieren, zur Verwendung des zentralen Powermanagements. Deaktivieren, um eine spezifische Leistungskonfiguration für dieses VTherm zu verwenden." + } + }, + "presence": { + "title": "Verwaltung Anwesenheit", + "description": "Geben Sie einen Präsenzsensor (Status true, wenn jemand anwesend ist) und die bei Abwesenheit zu verwendenden Zieltemperaturen an.", + "data": { + "presence_sensor_entity_id": "Anwesenheitssensor", + "use_presence_central_config": "Verwendung der zentralen Temperaturkonfiguration bei Abwesenheit. Abwählen, um spezifische Temperaturen zu verwenden." + }, + "data_description": { + "presence_sensor_entity_id": "Entity-ID vom Anwesenheitssensor" + } + }, + "advanced": { + "title": "Erweiterte Einstellungen", + "description": "Konfiguration der erweiterten Einstellungen. Belassen Sie die Standardwerte, wenn Sie nicht wissen, was Sie tun.\nDiese Einstellungen können zu Fehlfunktionen des Thermostats führen.", + "data": { + "safety_delay_min": "Maximale Zeitspanne zwischen zwei Temperaturmessungen", + "safety_min_on_percent": "Mindestprozentsatz der Leistung", + "safety_default_on_percent": "Prozentuale Leistungsaufnahme im Sicherheitsmodus", + "repair_incorrect_state": "Fehlerhafte Zustand reparieren", + "use_advanced_central_config": "Erweiterte zentrale Konfiguration verwenden" + }, + "data_description": { + "safety_delay_min": "Maximal zulässige Zeitspanne in Minuten zwischen zwei Temperaturmessungen. Nach Ablauf dieser Zeitspanne schaltet der Thermostat in den Sicherheitsmodus.", + "safety_min_on_percent": "Mindestprozentsatz für die Heizleistung, unterhalb dessen die Sicherheitsvoreinstellung niemals aktiviert wird.", + "safety_default_on_percent": "Standardwert für den Heizungsprozentsatz im Sicherheitsmodus. 0 schaltet den Heizkörper im Sicherheitsmodus aus.", + "repair_incorrect_state": "Repariert automatisch den fehlerhaften Zustand der zugrunde liegenden Entitäten, wenn dieser erkannt wird. Die Funktion sendet den gewünschten Befehl erneut, wenn der Zustand des zugrunde liegenden Geräts nicht den erwarteten Zustand erfüllt.", + "use_advanced_central_config": "Wenn aktiviert wird die zentrale erweiterte Konfiguration verwendet. Deaktivieren und Attribute eingeben um eine spezifische erweiterte Konfiguration zu verwenden." + } + }, + "central_boiler": { + "title": "Steuerung der Zentralheizung", + "description": "Geben Sie die Dienste an, die zum Ein- und Ausschalten des Zentralheizungskessels aufgerufen werden sollen. Der aufzurufende Dienst muss wie folgt formatiert sein: `entity_id/service_name[/attribut:wert]` (/attribut:wert ist optional)\nBeispiel:\n- zum Einschalten eines Schalters: `switch.kesselsteuerung/switch.turn_on`\n- zum Ausschalten eines Schalters: `switch.kesselsteuerung/switch. turn_off`\n- Um den Heizkessel auf 25 °C zu programmieren und somit sein Einschalten zu erzwingen: `climate.heizungsthermostat/climate.set_temperature/temperature:25`\n- Um 10 °C an den Heizkessel zu senden und somit sein Ausschalten zu erzwingen: `climate.heizungsthermostat/climate.set_temperature/temperature:10`", + "data": { + "central_boiler_activation_service": "Befehl zum Einschalten", + "central_boiler_deactivation_service": "Befehl zum Ausschalten", + "central_boiler_activation_delay_sec": "Verzögerung bei der Aktivierung (Sekunden)", + "keep_alive_boiler_delay_sec": "Verzögerung für Keep-Alive (Sekunden)" + }, + "data_description": { + "central_boiler_activation_service": "Befehl zum Einschalten des Zentralheizungskessels, im Format: entity_id/service_name[/attribut:wert]", + "central_boiler_deactivation_service": "Befehl zum Ausschalten des Zentralheizungskessels, Format: entity_id/service_name[/attribut:wert]", + "central_boiler_activation_delay_sec": "Verzögerung in Sekunden vor der Aktivierung des Zentralheizungskessels, wenn dieser eingeschaltet werden soll", + "keep_alive_boiler_delay_sec": "Verzögerung in Sekunden zum Weiterleiten des Kesselbefehls, wenn Keep-Alive aktiviert ist. Setzen Sie auf 0, um Keep-Alive zu deaktivieren." + } + }, + "valve_regulation": { + "title": "Selbstregulierung durch Ventil", + "description": "Konfiguration der Selbstregelung durch direkte Ventilsteuerung", + "data": { + "offset_calibration_entity_ids": "Offset Kalibrierung Entity-IDs", + "opening_degree_entity_ids": "Öffnungsweite Entity-IDs", + "closing_degree_entity_ids": "Schließweite Entity-IDs", + "proportional_function": "Algorithmus", + "opening_threshold_degree": "Öffnungsschwelle", + "min_opening_degrees": "Minimaler Öffnungsweite", + "max_opening_degrees": "Maximale Öffnungsweite", + "max_closing_degree": "Maximale Schließweite" + }, + "data_description": { + "offset_calibration_entity_ids": "Liste der 'Offset-Kalibrierungs'-Entities (offset calibration). Für eine bessere Regelung konfigurieren, wenn das TRV diese Entity besitzt. Es sollte eine pro Klima-Entity geben", + "opening_degree_entity_ids": "Liste der 'Ventil öffnen'-Entities. Es muss eine pro Klima-Entity geben", + "closing_degree_entity_ids": "Liste der 'Ventil schließen'-Entities. Für eine bessere Regelung konfigurieren, wenn das TRV über dies Funktion verfügt. Es muss eine pro Klima-Entity geben", + "proportional_function": "Zu verwendender Algorithmus (andere Algorithmen können als {plugins_link} installiert werden)", + "opening_threshold_degree": "Öffnungsschwelle des Ventils, unterhalb derer das Ventil als geschlossen gilt (und dann 'max_closing_degree' gilt)", + "min_opening_degrees": "Eine kommaseparierte Liste der Mindestöffnung. Standardwert ist 0. Beispiel: 20, 25, 30", + "max_opening_degrees": "Maximaler Öffnungsgrad für jedes zugeordnete Gerät, durch Kommas getrennt. Standardwert 100. Das Ventil öffnet sich niemals über diesen Wert hinaus. Beispiel: 80, 85, 90", + "max_closing_degree": "Maximale Schließung. Das Ventil wird niemals über diesen Wert hinaus geschlossen. Zum vollständigen Schliessen auf 100 einstellen." + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - Konfiguration", + "description": "Allgemeine Parameter für das Auto-TPI-Lernen.", + "data": { + "auto_tpi_learning_type": "Art des Lernens", + "heater_heating_time": "Aufheizzeit (min)", + "heater_cooling_time": "Auskühlzeit (min)", + "auto_tpi_heating_rate": "Aufheizgeschwindigkeit ({unit}/h)", + "auto_tpi_aggressiveness": "Aggressivität", + "auto_tpi_enable_advanced_settings": "Erweiterte Einstellungen aktivieren" + }, + "data_description": { + "auto_tpi_learning_type": "Wähle 'Erkundung' für den ersten Start (gewichteter Durchschnitt, Gewicht 1) oder 'Feinabstimmung' für kontinuierliche Anpassungen (EWMA, Alpha 0,08).", + "heater_heating_time": "Zeit bis zum Erreichen der vollen Leistung (Minuten)", + "heater_cooling_time": "Abkühlzeit nach dem Ausschalten (Minuten)\n\n| Typ | Aufheizzeit | Abkühlzeit |\n| :--- | :--- | :--- |\n| Elektroheizkörper | 5 Min. | 7 Min. |\n| Wasserheizkörper | 15 Min. | 20 Min. |\n| Fußbodenheizung | 30 Min. | 45 Min. |", + "auto_tpi_heating_rate": "Temperaturanstiegskapazität des Heizkörpers ({unit} pro Stunde). Kann über den Aktionsdienst 'Kapazität kalibrieren' geschätzt werden.", + "auto_tpi_aggressiveness": "Skalierungsfaktor für die gelernten Koeffizienten (50-100 %). Niedrigere Werte führen zu konservativeren Koeffizienten und verringern das Risiko einer Überschreitung des Sollwerts.", + "auto_tpi_enable_advanced_settings": "Aktivieren Sie diese Option, um die Feinabstimmung der Parameter (Alpha, Abklingen, Gewicht) vorzunehmen." + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - Gewichteter Durchschnitt", + "description": "Parameter für die gewichtete Durchschnittsmethode.", + "data": { + "auto_tpi_avg_initial_weight": "Anfangsgewicht" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Gewichtung des Anfangs-/Vorherwerts bei der Durchschnittsberechnung" + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA", + "description": "Parameter für die EWMA-Methode.\n\nEmpfehlungen :\n| Situation | Alpha (ema_alpha) | Abklingrate (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Lernstart | 0.15 | 0.08 |\n| Lernabschluß | 0.08 | 0.12 |\n| Kontinuierliches Lernen | 0.05 | 0.02 |", + "data": { + "auto_tpi_ema_alpha": "Alpha", + "auto_tpi_ema_decay_rate": "Rückgangsrate" + }, + "data_description": { + "auto_tpi_ema_alpha": "Glättungsfaktor (0-1). Höher = schnellere Anpassung", + "auto_tpi_ema_decay_rate": "Rate, mit der Alpha im Laufe der Zeit abnimmt (Stabilisierung)" + } + }, + "sync_device_internal_temp": { + "title": "Geräte-Innentemperatur synchronisieren", + "description": "Konfiguration zur Synchronisierung der Innentemperatur der zugeordneten Geräte mit ausgewählten Entitäten", + "data": { + "sync_with_calibration": "Offset-Kalibrierung anwenden", + "sync_entity_ids": "Für die Synchronisierung verwendete Entity-IDs" + }, + "data_description": { + "sync_with_calibration": "Aktiviert, wird die Offset-Kalibrierung bei der Synchronisierung der Innentemperatur angewenden. Deaktiviert, wird die Temperatur direkt aus den ausgewählten Entities kopiert.", + "sync_entity_ids": "Entitie-Liste, die zur Synchronisierung der internen Temperatur der zugeordneten Geräte verwendet werden. Es muss eine pro zugeordnetem Gerät vorhanden sein. Wenn es aktiviert ist, muss eine Kalibrierungsentität vorhanden sein, ansonsten eine Temperaturentity. Beides sind Entities vom Typ `number`." + } + }, + "heating_failure_detection": { + "title": "Erkennung von Heizungsausfällen", + "description": "Konfiguration der Heizungsausfallerkennung. Diese Funktion erkennt Anomalien, wenn Heizleistung angefordert wird, die Temperatur jedoch nicht steigt, oder wenn die Heizung ausgeschaltet ist, die Temperatur jedoch weiter steigt.", + "data": { + "use_heating_failure_detection_feature": "Heizungsausfallerkennung aktivieren", + "use_heating_failure_detection_central_config": "Zentrale Konfiguration der Fehlererkennung verwenden", + "heating_failure_threshold": "Heizungsausfallschwellwert", + "cooling_failure_threshold": "Kühlungsausfallschwellwert", + "heating_failure_detection_delay": "Erkennungszeit (Minuten)", + "temperature_change_tolerance": "Toleranze (°C)", + "failure_detection_enable_template": "Template zur Aktivierung der Erkennung" + }, + "data_description": { + "use_heating_failure_detection_feature": "Aktiviert die Erkennung von Heizungsstörungen für VTherms mit TPI.", + "use_heating_failure_detection_central_config": "Bei aktivierter Option wird zentrale Konfiguration für die Fehlererkennung verwendet. Deaktivieren, um spezifische Einstellungen für diesen VTherm zu verwenden.", + "heating_failure_threshold": "Schwellenwert in Prozent, ab dem die Heizung die Temperatur erhöhen sollte (0,9 = 90 %)", + "cooling_failure_threshold": "Schwellenwert in Prozent, unter dem die Temperatur nicht steigen sollte (0,0 = 0 %)", + "heating_failure_detection_delay": "Wartezeit in Minuten, ehe überprüft wird, ob sich die Temperatur wie erwartet verändert hat", + "temperature_change_tolerance": "Minimale Temperaturänderung in Grad, die als aussagekräftig angesehen wird. Kleinere Änderungen werden ignoriert, um Störsignale der Sensoren herauszufiltern (Standardwert: 0,5 °C).", + "failure_detection_enable_template": "Ein optionales Jinja2-Template, das 'True' zurückgeben muss, um die Erkennung zu aktivieren. Nützlich, um die Erkennung vorübergehend zu deaktivieren, wenn eine externe Wärmequelle aktiv ist (z. B. `is_state('binary_sensor.holzofen', 'off')` zwischen doppelten geschweiften Klammern). Wenn leer, ist die Erkennung immer aktiv." + } + } + }, + "error": { + "unknown": "Unerwarteter Fehler", + "unknown_entity": "Unbekannte Entity-ID", + "window_open_detection_method": "Es darf nur eine Methode zur Erkennung von offenen Fenstern verwendet werden. Verwenden Sie entweder den Fenstersensor oder die Temperaturschwelle, aber nicht beides.", + "no_central_config": "'Zentrale Konfiguration verwenden' kann nicht aktiviert werden, da keine zentrale Konfiguration gefunden wurde. Es muss erst ein Versatile Thermostat des Typs 'Zentrale Konfiguration' erstellt werden, um ihn verwenden zu können.", + "service_configuration_format": "Falsches Format der Servicekonfiguration", + "valve_regulation_nb_entities_incorrect": "Die Anzahl der Entities für die Ventilregelung muss gleich der Anzahl der zugeordneten Entities sein", + "sync_device_internal_temp_nb_entities_incorrect": "Die Anzahl der Entities zur Synchronisierung der internen Gerätetemperatur muss der Anzahl der zugeordneten Geräte entsprechen", + "min_opening_degrees_format": "Es wird eine Liste positiver Ganzzahlen erwartet, die durch Komma getrennte sind. Beispiel: 20, 25, 30", + "max_opening_degrees_format": "Es wird eine Liste positiver Ganzzahlen zwischen 0 und der maximalen Ventilöffnung erwartet, die durch Kommas getrennt sind. Beispiel: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Für jedes zugeordnete Gerät muss max_opening_degrees absolut größer als min_opening_degrees sein. Bitte die Konfiguration überprüfen.", + "vswitch_configuration_incorrect": "Die Konfiguration der Befehlsanpassung ist falsch. Sie ist für nicht umschaltbare Basiswerte erforderlich und muss das Format 'service_name[/attribut:wert]' haben. Weitere Informationen in der README-Datei." + }, + "abort": { + "already_configured": "Gerät ist bereits konfiguriert" + } + }, + "options": { + "flow_title": "Versatile Thermostat Konfiguration", + "step": { + "user": { + "title": "Typ - {name}", + "description": "Zu erstellenden Thermostattyp auswählen.", + "data": { + "thermostat_type": "Thermostattyp" + }, + "data_description": { + "thermostat_type": "Es ist nur ein Thermostat vom Typ zentrale Konfiguration möglich" + } + }, + "menu": { + "title": "Menü", + "description": "Konfiguration des Thermostats. Wenn alle erforderlichen Parameter eingegeben sind, kann die Konfiguration abgeschlossen werden.", + "menu_options": { + "main": "Grundlegendes (Name, Temperatursensor...)", + "central_boiler": "Zentralheizung", + "type": "Gerätezuordnung", + "tpi": "TPI-Parameter", + "features": "Funktionen", + "presets": "Temperaturvoreinstellungen", + "window": "Fensteröffnugserkennung", + "motion": "Bewegungserkennung", + "power": "Powermanagement", + "presence": "Anwesenheitserkennung", + "advanced": "Erweiterte Einstellungen", + "auto_start_stop": "Auto Start/Stopp", + "valve_regulation": "Ventilsteuerung", + "sync_device_internal_temp": "Synchronisierung der Gerätetemperatur", + "lock": "Sperrenverwaltung", + "heating_failure_detection": "Heizungsausfallerkennung", + "finalize": "Abschliessen", + "configuration_not_complete": "Konfiguration unvollständig" + } + }, + "lock": { + "title": "Verwaltung der Sperre - {name}", + "description": "Die Sperrfunktion verhindert Änderungen an einem Thermostat über die Benutzeroberfläche oder Automatisierungen, während der Thermostat betriebsbereit bleibt.", + "data": { + "use_lock_central_config": "Zentrale Sperrkonfiguration verwenden", + "lock_code": "Optionaler PIN (4 Ziffern)", + "lock_users": "Sperre verhindert Benutzerbefehle", + "lock_automations": "Sperre verhindert Befehle von Automatisierungen und Integrationen (z.B. Planer)", + "auto_relock_sec": "Automatische Wiederverriegelung" + }, + "data_description": { + "use_lock_central_config": "Aktivieren für zentrale Sperrkonfiguration. Deaktivieren für spezifische Sperrkonfiguration dieses VTherm", + "auto_relock_sec": "Verzögerung in Sekunden bevor nach dem Entsperren automatisch wieder verriegelt wird. 0 zum Deaktivieren. Standardwert ist 30 Sekunden." + } + }, + "main": { + "title": "Attribute - {name}", + "description": "Wichtigste erforderliche Attribute", + "data": { + "name": "Name", + "thermostat_type": "Thermostattyp", + "temperature_sensor_entity_id": "Temperatursensor", + "last_seen_temperature_sensor_entity_id": "Temperatursensor zuletzt gesehen", + "external_temperature_sensor_entity_id": "Aussentemperatursensor", + "cycle_min": "Zyklusdauer (Minuten)", + "temp_min": "Zulässige Mindesttemperatur", + "temp_max": "Zulässige Höchsttemperatur", + "step_temperature": "Temperaturschritte", + "device_power": "Geräteleistung", + "use_central_mode": "Aktiviert die Steuerung durch eine Zentrale (benötigt zentrale Konfiguration). Aktivieren, um die Steuerung des VTherm über die Dropdown-Liste 'central_mode' der zentralen Konfigurationseinheit zuzulassen.", + "use_main_central_config": "Zusätzliche zentrale Konfiguration benutzen. Auswählen zur Benutzung der zusätzlichen zentralen Konfiguration (Aussentemperatur, Minimum, Maximum, Schrittweite, ...).", + "used_by_controls_central_boiler": "Benutze Zentralheizung. Anwählen, wenn VTerm die Kontrolle über die Zentralheizung haben soll." + }, + "data_description": { + "cycle_min": "Minimale Dauer eines Heiz-/Kühlzyklus in Minuten, um zu häufiges Schalten zu vermeiden", + "temp_min": "Zulässige Mindesttemperatur für das Thermostat", + "temp_max": "Zulässige Höchsttemperatur für das Thermostat", + "step_temperature": "Temperaturschritt für die Einstellung der Zieltemperatur", + "temperature_sensor_entity_id": "Raumtemperatursensor Entity-ID", + "last_seen_temperature_sensor_entity_id": "Entitäts-ID des Sensors, die das Datum und die Uhrzeit der letzten Meldung des Temperatursensors angibt. Der Status muss im Format Datum Uhrzeit angegeben werden (z. B.: 2024-03-31T17:07:03+00:00).", + "external_temperature_sensor_entity_id": "Entity-ID des Außentemperatursensors. Wird nicht verwendet, wenn eine zentrale Konfiguration definiert ist." + } + }, + "features": { + "title": "Funktionen - {name}", + "description": "Zu verwendende Thermostatfunktionen", + "data": { + "use_window_feature": "Mit Fensteröffnungserkennung", + "use_motion_feature": "Mit Bewegungserkennung", + "use_power_feature": "Mit Powermanagement", + "use_presence_feature": "Mit Anwesenheitserkennung", + "use_central_boiler_feature": "Benutze Zentralheizung. Aktivieren, um eine Steuerung für eine Zentralheizung hinzuzufügen. Anschließend muss VTerm für die Steuerung der Zentralheizung konfiguriert werden, um wirksam zu werden. Wenn mindestens einer der VTerms heizen muss, wird der Heizkessel angeschalten. Wenn kein VTherm heizen muss, wird sie abgeschalten. Die Befehle zum Ein- und Ausschalten der Zentralheizung finden sie auf der folgenden Konfigurationsseite.", + "use_auto_start_stop_feature": "Mit automatischer Start- und Stopfunktion.", + "use_heating_failure_detection_feature": "Mit Heizungsausfallerkennung" + } + }, + "type": { + "title": "Zugehörige Entities - {name}", + "description": "Attribute der zugeordneten Entities", + "data": { + "underlying_entity_ids": "Die zu steuernden Geräte", + "heater_keep_alive": "Keep-alive Schaltinterval in Sekunden", + "proportional_function": "Algorithmus", + "ac_mode": "AC-Modus (Klimaanlage)?", + "sync_device_internal_temp": "Synchronisierung der internen Gerätetemperatur", + "auto_regulation_mode": "Selbstregelnd", + "auto_regulation_dtemp": "Regelschwelle", + "auto_regulation_periode_min": "Minimale Regelzeit", + "auto_regulation_use_device_temp": "Ausgleich der Innentemperatur vom Gerät", + "inverse_switch_command": "Umgekehrter Schaltbefehl", + "auto_fan_mode": "Automatischer Lüfterbetrieb", + "on_command_text": "Anpassung der Einschaltsteuerung", + "vswitch_on_command": "Einschaltbefehl (optional)", + "off_command_text": "Anpassung der Abschaltsteuerung", + "vswitch_off_command": "Abschaltbefehl (optional)" + }, + "data_description": { + "underlying_entity_ids": "Liste der Geräte, die von diesem VTherm kontrolliert werden", + "heater_keep_alive": "Aktualisierungsintervall des Heizungsschalters in Sekunden. Nur für Schalter verwenden, die dies erfordern", + "proportional_function": "Zu verwendender Algorithmus (andere Algorithmen können als {plugins_link} installiert werden)", + "ac_mode": "Benutze Klimaanlagenmodus (AC)", + "sync_device_internal_temp": "Die Innentemperatur des zugeordneten Geräts mit der vom VTherm gemessenen Temperatur synchronisieren", + "auto_regulation_mode": "Verwendung der Selbstregulierung durch VTherm", + "auto_regulation_dtemp": "Der Schwellenwert in ° (oder % bei Ventilen), unter dem die Regelung nicht aktiviert wird", + "auto_regulation_periode_min": "Dauer in Minuten zwischen zwei Regelungsaktualisierungen", + "auto_regulation_use_device_temp": "Ausgleich der eventuell vorhandenen Innentemperatur vom zugeordneten Gerät benutzen, zur Beschleunigung der Selbstregulierung (ist in den meisten Fällen nicht erforderlich)", + "inverse_switch_command": "Kehrt die Schaltersteuerung bei Installationen mit Pilotdraht und Diode um.", + "auto_fan_mode": "Aktiviert die Belüftung automatisch bei großen Abweichugnen.", + "on_command_text": "Für zugeordnete Geräte des Typs `select` oder `climate`." + } + }, + "tpi": { + "title": "TPI (Zeitproportionales Integral) - {name}", + "description": "Attribute des Algorithmus 'zeitproportionales Integral'", + "data": { + "tpi_coef_int": "coeff_int", + "tpi_coef_ext": "coeff_ext", + "tpi_threshold_low": "TPI untere Schwelle", + "tpi_threshold_high": "TPI obere Schwelle", + "minimal_activation_delay": "Mindestlaufzeit", + "minimal_deactivation_delay": "Mindestabschaltzeit", + "use_tpi_central_config": "Zentrale TPI-Konfiguration verwenden", + "auto_tpi_mode": "Auto-TPI-Lernen aktivieren" + }, + "data_description": { + "tpi_coef_int": "Zu verwendender Faktor für das interne Temperaturdelta", + "tpi_coef_ext": "Zu verwendender Faktor für das externe Temperaturdelta", + "tpi_threshold_low": "Schwellenwert in °, unter dem der TPI aktiviert wird. 0 bedeutet kein Schwellenwert.", + "tpi_threshold_high": "Schwellenwert in °, über dem der TPI-Algorithmus deaktiviert wird. 0 bedeutet kein Schwellenwert.", + "use_tpi_central_config": "Aktivieren, um die zentrale TPI-Konfiguration zu benutzen. Deaktivieren, zur Verwendung einer spezifischen TPI-Konfiguration.", + "minimal_activation_delay": "Zeit in Sekunden, unterhalb derer das Gerät nicht aktiviert wird", + "minimal_deactivation_delay": "Zeit in Sekunden, nach der das Gerät in den Ruhezustand wechselt", + "auto_tpi_mode": "TPI-Lernsitzungen werden manuell gestartet; bitte zuvor die Dokumentation lesen." + } + }, + "presets": { + "title": "Voreinstellungen - {name}", + "description": "Auswählen, wenn das Thermostat die zentrale Voreinstellungen verwenden soll. Deaktivieren und der Thermostat hat seine eigenen Voreinstellungseinheiten.", + "data": { + "use_presets_central_config": "Zentrale Voreinstellungen verwenden" + } + }, + "window": { + "title": "Fenster - {name}", + "description": "Schaltet den Heizkörper aus, wenn das Fenster offen ist.\nIst keine Entity-ID angegeben, wird die automatische Erkennung von geöffneten Fenstern verwendet.", + "data": { + "window_sensor_entity_id": "Öffnungssensor (Entity-ID)", + "window_delay": "Verzögerung bei 'Offen' (Sekunden)", + "window_off_delay": "Verzögerung bei 'Geschlossen' (Sekunden)", + "window_auto_open_threshold": "Obere Temperaturabfallschwelle für die automatische Erkennung (in °/Stunden)", + "window_auto_close_threshold": "Untere Temperaturabfallschwelle für das Ende der automatischen Erkennung (in °/Stunden)", + "window_auto_max_duration": "Maximale Dauer einer automatischen Abschaltung (in Minuten)", + "use_window_central_config": "Zentrale Fensteröffnungskonfiguration verwenden", + "window_action": "Aktion" + }, + "data_description": { + "window_sensor_entity_id": "Leer lassen, wenn kein Fenstersensor vorhanden ist, dafür aber die automatische Erkennung verwendet werden soll.", + "window_delay": "Die Verzögerung (in Sekunden), bevor die Änderung berücksichtigt werden soll.", + "window_off_delay": "Die Verzögerung in Sekunden, bevor die Änderung des Sensors bei Erkennung eines Schließvorgangs berücksichtigt wird. Das Feld leer lassen, um die gleiche Verzögerung beim Öffnen und Schließen zu verwenden..", + "window_auto_open_threshold": "Empfohlener Wert: zwischen 3 und 10. Das Feld leer lassen, wenn Sie die automatische Erkennung nicht verwenden.", + "window_auto_close_threshold": "Empfohlener Wert: 0. Das Feld leer lassen, wenn die automatische Erkennung nicht verwendet wird.", + "window_auto_max_duration": "Empfohlener Wert: 60 (1 Stunde). Das Feld leer lassen, wenn die automatische Erkennung nicht verwendet wird", + "use_window_central_config": "Ist diese Option aktiviert, wird die zentrale Konfiguration verwendet. Wenn deaktiviert, sind Attribute einzugeben, um eine spezifische Öffnungskonfiguration zu verwenden", + "window_action": "Aktion, die ausgeführt werden soll, wenn öffnen erkannt wird" + } + }, + "motion": { + "title": "Bewegung (Motion) - {name}", + "description": "Die Voreinstellung passt sich automatisch an, wenn eine Bewegung erkannt wird.\n'Voreinstellung bei Bewegung' und 'Voreinstellung ohne Bewegung müssen mit den zu verwendenden Voreinstellungen ausgewählt werden.", + "data": { + "motion_sensor_entity_id": "Bewegungssensor", + "motion_delay": "Einschaltverzögerung", + "motion_off_delay": "Ausschaltverzögerung", + "motion_preset": "Voreinstellung bei Bewegung", + "no_motion_preset": "Voreinstellung ohne Bewegung", + "use_motion_central_config": "Zentrale Bewegungskonfiguration verwenden" + }, + "data_description": { + "motion_sensor_entity_id": "Entity-ID des Bewegungssensors", + "motion_delay": "Verzögerung vor Aktivierung der Erkennung einer Bewegung (Sekunden)", + "motion_off_delay": "Verzögerung vor Deaktivierung wenn keine Bewegung erkannt wird (Sekunden)", + "motion_preset": "Voreinstellung bei Bewegungserkennung", + "no_motion_preset": "Voreinstellung, die verwendet werden soll, wenn keine Bewegung erkannt wird", + "use_motion_central_config": "Aktivieren, um die zentralen Konfiguration zu benutzen. Deaktivieren und Attibute eingeben für eine spezifische Konfiguration." + } + }, + "power": { + "title": "Powermanagement - {name}", + "description": "Wählt automatisch die Voreinstellung 'power', wenn der Stromverbrauch ein Maximum überschreitet. Gib ie die Entity-IDs der Sensoren an, die die Gesamtleistung und die maximal zulässige Leistung messen. Gib anschließend die Leistung des Geräts an. Alle Sensoren und der Stromverbrauch des Geräts müssen dieselbe Maßeinheit haben (kW oder W)", + "data": { + "power_sensor_entity_id": "Gesamtleistung", + "max_power_sensor_entity_id": "Leistungssensor Max (Entity-ID)", + "power_temp": "Temperatur bei Lastabwurf", + "use_power_central_config": "Benutze zentrales Powermanagement" + }, + "data_description": { + "power_sensor_entity_id": "Entity-ID des Gesamtleistungssensors der Wohnung/des Haus", + "max_power_sensor_entity_id": "Entity-ID des Leistungssensors Maximal zulässige Leistung vor Lastabwurf", + "power_temp": "Zieltemperatur bei Lastabwurf", + "use_power_central_config": "Aktivieren, zur Verwendung des zentralen Powermanagements. Deaktivieren und Attribute eingeben für eine spezifische Konfiguration." + } + }, + "presence": { + "title": "Anwesenheit - {name}", + "description": "Gib einen Präsenzsensor (true, wenn jemand anwesend ist) und die bei Abwesenheit zu verwendenden Zieltemperaturen an.", + "data": { + "presence_sensor_entity_id": "Anwesenheitssensor", + "use_presence_central_config": "Zentrale Temperaturkonfiguration bei Abweseheit benutzen. Nicht setzen bei dedizierten Temperatureinstellungen." + }, + "data_description": { + "presence_sensor_entity_id": "Anwesenheitssensor Entity-ID" + } + }, + "advanced": { + "title": "Erweitert - {name}", + "description": "Konfiguration der erweiterten Parameter. Belassen Sie die Standardwerte, wenn Sie nicht wissen, was Sie tun.\nDiese Einstellugen können zu Fehlfunktionen des Thermostats führen.", + "data": { + "minimal_activation_delay": "Minimale Aktivierungsverzögerung", + "minimal_deactivation_delay": "Minimale Deaktivierungsverzögerung", + "safety_delay_min": "Maximale Zeitspanne zwischen zwei Temperaturmessungen", + "safety_min_on_percent": "Mindestleistungsanteil in Prozent", + "safety_default_on_percent": "Prozentuale Leistungsaufnahme im Sicherheitsmodus", + "repair_incorrect_state": "Fehlerhafte Zustand reparieren", + "use_advanced_central_config": "Erweiterte zentrale Konfiguration verwenden" + }, + "data_description": { + "minimal_activation_delay": "Verzögerungszeit in Sekunden, unter der das Gerät nicht aktiviert wird", + "minimal_deactivation_delay": "Verzögerungszeit in Sekunden, nach deren Ablauf das Gerät aktiv bleibt", + "safety_delay_min": "Maximal zulässige Zeitspanne in Minuten zwischen zwei Temperaturmessungen. Nach Ablauf dieser Zeitspanne schaltet der Thermostat in den Sicherheitsmodus.", + "safety_min_on_percent": "Mindestprozentsatz für die Heizleistung, unter der die Sicherheitsvoreinstellung niemals aktiviert wird.", + "safety_default_on_percent": "Standardwert der Heizleistung in Prozent im Sicherheitsmodus. Auf 0 setzen, um den Heizkörper im Sicherheitsmodus auszuschalten.", + "repair_incorrect_state": "Repariert automatisch den fehlerhaften Zustand der zugrunde liegenden Entitäten, wenn dieser erkannt wird. Die Funktion sendet den gewünschten Befehl erneut, wenn der Zustand des zugrunde liegenden Geräts nicht den erwarteten Zustand erfüllt.", + "use_advanced_central_config": "Aktivieren, um die erweiterte zentrale Konfiguration zu verwenden. Deaktivieren und Attribute eingeben, wenn eine separate erweiterte Konfiguration verwendet werden soll." + } + }, + "central_boiler": { + "title": "Steuerung der Zentralheizung - {name}", + "description": "Geben Sie die Dienste ein, die zum Ein- und Ausschalten der Zentralheizung benutzt werden sollen. Der verwendete Dienst muss wie folgt formatiert sein: `entity_id/service_name[/attribute:wert]` (/attribute:wert ist optional)\nBeispiel:\n- zum Einschalten eines Schalters: `switch.control_heizung/switch.turn_on`\n- zum ausschalten eines Schalter: `switch.control_heizung/switch.turn_off`\n- Um den Kessel auf 25° zu programmieren und somit ein Einschalten zu erzwingen: `climate.thermostat_heizung/climate.set_temperature/temperature:25`\n- Um 10° an den Heizessel zu senden und so sein Ausschalten zu erzwingen: `climate.thermostat_heizung/climate.set_temperature/temperature:10`", + "data": { + "central_boiler_activation_service": "Einschaltbefehl", + "central_boiler_deactivation_service": "Ausschaltbefehl", + "central_boiler_activation_delay_sec": "Verzögerung bei der Aktivierung (Sekunden)", + "keep_alive_boiler_delay_sec": "Verzögerung für Keep-Alive (Sekunden)" + }, + "data_description": { + "central_boiler_activation_service": "Befehl zum Einschalten des Zentralheizungskessels, im Format: entity_id/service_name[/attribut:wert]", + "central_boiler_deactivation_service": "Befehl zum Ausschalten des Zentralheizungskessels, Format: entity_id/service_name[/attribut:wert]", + "central_boiler_activation_delay_sec": "Verzögerung in Sekunden vor der Aktivierung des Zentralheizungskessels, wenn dieser eingeschaltet werden soll", + "keep_alive_boiler_delay_sec": "Verzögerung in Sekunden zum Weiterleiten des Kesselbefehls, wenn Keep-Alive aktiviert ist. Setzen Sie auf 0, um Keep-Alive zu deaktivieren." + } + }, + "valve_regulation": { + "title": "Selbstregulierung mit Ventil - {name}", + "description": "Konfiguration für Selbstregulierung durch direkte Ventilsteuerung", + "data": { + "offset_calibration_entity_ids": "Entities zur 'Offset-Kalibrierung'", + "opening_degree_entity_ids": "Entities 'Ventil öffnen'", + "closing_degree_entity_ids": "Entities 'Ventil schliessen'", + "proportional_function": "Algorithmus", + "opening_threshold_degree": "Öffnungsschwelle", + "min_opening_degrees": "Minimaler Öffnungsweite", + "max_opening_degrees": "Maximale Öffnungsweite", + "max_closing_degree": "Maximale Schließweite" + }, + "data_description": { + "offset_calibration_entity_ids": "Liste der 'Offset-Kalibrierung'-Entities. Wenn das TRV über diese Funktion verfügen, bitte dieses für eine bessere Regelung konfigurieren. Es muss eine pro zugeordneter Klima-Entity vorhanden sein", + "opening_degree_entity_ids": "Liste der 'Ventilöffnung'-Entities. Es muss eine pro zugeordneter Klima-Entity geben", + "closing_degree_entity_ids": "Liste der 'Ventil schliessen'-Entities. Wenn das TRV über diese Funktion verfügen, bitte dieses für eine bessere Regelung konfigurieren. Es muss eine pro zugeordneter Klima-Entity vorhanden sein", + "proportional_function": "Zu verwendender Algorithmus (andere Algorithmen können als {plugins_link} installiert werden)", + "opening_threshold_degree": "Öffnungsschwelle des Ventils, unterhalb derer das Ventil als geschlossen gilt (und dann 'max_closing_degree' gilt)", + "min_opening_degrees": "Eine kommaseparierte Liste der Mindestöffnung. Standardwert ist 0. Beispiel: 20, 25, 30", + "max_opening_degrees": "Maximaler Öffnungsgrad für jedes zugeordnete Gerät, durch Kommas getrennt. Standardwert 100. Das Ventil öffnet sich niemals über diesen Wert hinaus. Beispiel: 80, 85, 90", + "max_closing_degree": "Maximale Schließung. Das Ventil wird niemals über diesen Wert hinaus geschlossen. Bei einem Wert von 100, schliesst das Ventil vollständig." + } + }, + "sync_device_internal_temp": { + "title": "Synchronisierung der internen Temperatur der zugeordneten Geräte", + "description": "Konfiguration der Synchronisierung der internen Temperaturen der zugeordneten Geräte", + "data": { + "sync_with_calibration": "Anwendung einer Kalibrierung", + "sync_entity_ids": "Für die Synchronisierung verwendete Entity-IDs" + }, + "data_description": { + "sync_with_calibration": "Ist die Option aktiviert, wird die Offset-Kalibrierung bei der Synchronisierung der Innentemperatur angewendet. Deaktivieren, um die Temperatur direkt in die ausgewählten Elemente zu kopieren.", + "sync_entity_ids": "Entitie-Liste, die zur Synchronisierung der internen Temperatur der zugeordneten Geräte verwendet werden. Es muss eine pro zugeordnetem Gerät vorhanden sein. Wenn es aktiviert ist, muss eine Kalibrierungsentität vorhanden sein, ansonsten eine Temperaturentity. Beides sind Entities vom Typ `number`." + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - Konfiguration - {name}", + "description": "Allgemeine Parameter für das Auto-TPI-Lernen.", + "data": { + "auto_tpi_learning_type": "Art des Lernens", + "auto_tpi_heating_rate": "Aufheizrate ({unit}/h)", + "heater_heating_time": "Aufheizzeit (min)", + "heater_cooling_time": "Abkühlzeit (min)", + "auto_tpi_aggressiveness": "Aggressivität", + "auto_tpi_enable_advanced_settings": "Erweiterte Einstellungen aktivieren" + }, + "data_description": { + "auto_tpi_learning_type": "Wähle 'Erkundung' für den ersten Start (gewichteter Durchschnitt, Gewicht 1) oder 'Feinabstimmung' für kontinuierliche Anpassungen (EWMA, Alpha 0,08).", + "auto_tpi_heating_rate": "Temperaturanstiegskapazität des Heizkörpers ({unit} pro Stunde). Kann über den Aktionsdienst 'Kapazität kalibrieren' geschätzt werden.", + "heater_heating_time": "Zeit bis zum Erreichen der vollen Leistung (Minuten)", + "heater_cooling_time": "Abkühlzeit nach dem Ausschalten (Minuten)\n\n| Typ | Aufheizzeit | Abkühlzeit |\n| :--- | :--- | :--- |\n| Elektroheizkörper | 5 Min. | 7 Min. |\n| Wasserheizkörper | 15 Min. | 20 Min. |\n| Fußbodenheizung | 30 Min. | 45 Min. |", + "auto_tpi_aggressiveness": "Skalierungsfaktor für die gelernten Koeffizienten (50–100 %). Niedrigere Werte führen zu konservativeren Koeffizienten und verringern das Risiko einer Überschreitung des Sollwerts.", + "auto_tpi_enable_advanced_settings": "Kontrollkästchen aktivieren, um die Parameter des ausgewählten Algorithmus zu ändern." + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - Gewichteter Durchschnitt - {name}", + "description": "Paremeter für die 'Gewichteter Durchschnitt'-Methode", + "data": { + "auto_tpi_calculation_method": "Initialgewicht" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Anfängliche Gewichtung der vorhandenen Koeffizienten bei der Berechnung des Durchschnitts (typischerweise 1-50). Je höher die Gewichtung, desto langsamer und stabiler ist der Lernprozess." + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA - {name}", + "description": "Parameter für die EWMA-Methode.\n\nEmpfehlungen:\n| Situation | Alpha (ema_alpha) | Abklingrate (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Anfängliches Lernen | 0,15 | 0,08 |\n| Endgültiges Lernen | 0,08 | 0,12 |\n| Kontinuierliches Lernen | 0,05 | 0,02 |", + "data": { + "auto_tpi_ema_alpha": "Alpha", + "auto_tpi_ema_decay_rate": "Rückgangsrate" + }, + "data_description": { + "auto_tpi_ema_alpha": "Glättungsfaktor (0-1). Höher = schnellere Anpassung", + "auto_tpi_ema_decay_rate": "Rate, mit der Alpha im Laufe der Zeit abnimmt (Stabilisierung)" + } + }, + "heating_failure_detection": { + "title": "Erkennung eines Heizungsausfalls - {name}", + "description": "Konfiguration der Heizungsausfallerkennung. Diese Funktion erkennt Anomalien, wenn Heizleistung angefordert wird, die Temperatur jedoch nicht steigt, oder wenn die Heizung ausgeschaltet ist, die Temperatur jedoch weiter steigt.", + "data": { + "use_heating_failure_detection_feature": "Heizungsausfallerkennung aktivieren", + "use_heating_failure_detection_central_config": "Zentrale Konfiguration der Fehlererkennung verwenden", + "heating_failure_threshold": "Heizungsausfallschwelle", + "cooling_failure_threshold": "Kühlungsausfallschwelle", + "heating_failure_detection_delay": "Erkennungszeit (Minuten)", + "temperature_change_tolerance": "Toleranz (°C)", + "failure_detection_enable_template": "Template zur Aktivierung der Erkennung" + }, + "data_description": { + "use_heating_failure_detection_feature": "Aktivieren der Erkennung von Heizungsstörungen für VTherms mit TPI.", + "use_heating_failure_detection_central_config": "Aktivieren, um die zentrale Konfiguration für die Fehlererkennung zu verwenden. Deaktivieren, um spezifische Einstellungen bei diesen VTherm zu verwenden.", + "heating_failure_threshold": "Schwellenwert in Prozent, ab dem die Heizung die Temperatur erhöhen sollte (0,9 = 90 %)", + "cooling_failure_threshold": "Schwellenwert in Prozent, unter dem die Temperatur nicht steigen sollte (0,0 = 0 %)", + "heating_failure_detection_delay": "Wartezeit in Minuten, ehe überprüft wird, ob sich die Temperatur wie erwartet verändert hat", + "temperature_change_tolerance": "Minimale Temperaturänderung in Grad, die als aussagekräftig angesehen wird. Kleinere Änderungen werden ignoriert, um Störsignale der Sensoren herauszufiltern (Standardwert: 0,5 °C).", + "failure_detection_enable_template": "Ein optionales Jinja2-Template, das 'True' zurückgeben muss, um die Erkennung zu aktivieren. Nützlich, um die Erkennung vorübergehend zu deaktivieren, wenn eine externe Wärmequelle aktiv ist (z. B. `is_state('binary_sensor.holzofen', 'off')` zwischen doppelten geschweiften Klammern). Wenn leer, ist die Erkennung immer aktiv." + } + } + }, + "error": { + "unknown": "Unerwarteter Fehler", + "unknown_entity": "Unbekannte Entity-ID", + "window_open_detection_method": "Es darf nur eine Methode zur Erkennung von offenen Fenstern verwendet werden. Verwenden Sie entweder den Fenstersensor oder die Temperaturschwelle, aber nicht beides.", + "no_central_config": "'Zentrale Konfiguration verwenden' kann nicht aktiviert werden, da keine zentrale Konfiguration gefunden wurde. Es muss erst ein Versatile Thermostat des Typs 'Zentrale Konfiguration' erstellt werden, um ihn verwenden zu können.", + "service_configuration_format": "Falsches Format der Servicekonfiguration", + "valve_regulation_nb_entities_incorrect": "Die Anzahl der Entities für die Ventilregelung muss gleich der Anzahl der zugeordneten Entities sein", + "sync_device_internal_temp_nb_entities_incorrect": "Die Anzahl der Entities zur Synchronisierung der internen Gerätetemperatur muss der Anzahl der zugrunde liegenden Geräte entsprechen", + "min_opening_degrees_format": "Es wird eine Liste positiver Ganzzahlen erwartet, die durch Komma getrennte sind. Beispiel: 20, 25, 30", + "max_opening_degrees_format": "Es wird eine Liste positiver Ganzzahlen zwischen 0 und der maximalen Ventilöffnung erwartet, die durch Kommas getrennt sind. Beispiel: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Für jedes zugeordnete Gerät muss max_opening_degrees absolut größer als min_opening_degrees sein. Bitte die Konfiguration überprüfen.", + "vswitch_configuration_incorrect": "Die Konfiguration der Befehlsanpassung ist falsch. Sie ist für nicht umschaltbare Basiswerte erforderlich und muss das Format 'service_name[/attribut:wert]' haben. Weitere Informationen dazu in der README-Datei." + }, + "abort": { + "already_configured": "Das Gerät ist bereits konfiguriert" + } + }, + "selector": { + "thermostat_type": { + "options": { + "thermostat_central_config": "Zentrale Konfiguration", + "thermostat_over_switch": "Thermostat an einem Schalter", + "thermostat_over_climate": "Thermostat an einem Thermostat", + "thermostat_over_valve": "Thermostat an einem Ventil" + } + }, + "auto_regulation_mode": { + "options": { + "auto_regulation_slow": "Langsam", + "auto_regulation_strong": "Stark", + "auto_regulation_medium": "Mittel", + "auto_regulation_light": "Leicht", + "auto_regulation_expert": "Experte", + "auto_regulation_none": "Keine", + "auto_regulation_valve": "Direkte Ventilsteuerung" + } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "Ohne Lüfterautomatik", + "auto_fan_low": "Schwach", + "auto_fan_medium": "Mittel", + "auto_fan_high": "Hoch", + "auto_fan_turbo": "Turbo" + } + }, + "window_action": { + "options": { + "window_turn_off": "Ausschalten", + "window_fan_only": "Nur Lüfter", + "window_frost_temp": "Frostschutz", + "window_eco_temp": "Eco" + } + }, + "presets": { + "options": { + "frost": "Frostschutz", + "eco": "Eco", + "comfort": "Komfort", + "boost": "Boost" + } + }, + "auto_start_stop": { + "options": { + "auto_start_stop_none": "Deaktiviert", + "auto_start_stop_very_slow": "Sehr langsam", + "auto_start_stop_slow": "Langsam", + "auto_start_stop_medium": "Mittel", + "auto_start_stop_fast": "Schnell" + } + }, + "auto_tpi_calculation_method": { + "options": { + "average": "Durchschnitt", + "ema": "Exponentialer gleitender Durchschnitt (EMA)" + } + }, + "auto_tpi_learning_type": { + "options": { + "discovery": "Erkundung", + "fine_tuning": "Feinjustierung" + } + } + }, + "entity": { + "climate": { + "versatile_thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "power": "Lastabwurf", + "safety": "Sicherheit", + "none": "Händisch", + "frost": "Frostschutz" + } + } + } + } + }, + "binary_sensor": { + "safety_state": { + "name": "Sicherheitsstatus" + }, + "heating_failure_state": { + "name": "Heizstatus" + }, + "overpowering_state": { + "name": "Lastabwurfstatus" + }, + "window_state": { + "name": "Fensterstatus" + }, + "motion_state": { + "name": "Bewegungsstatus" + }, + "presence_state": { + "name": "Anwesenheitsstatus" + }, + "window_bypass_state": { + "name": "Fenster-Bypass" + }, + "central_boiler_state": { + "name": "Zentralheizungskessel" + } + }, + "number": { + "frost_temp": { + "name": "Frostschutz" + }, + "eco_temp": { + "name": "Eco" + }, + "comfort_temp": { + "name": "Komfort" + }, + "boost_temp": { + "name": "Boost" + }, + "frost_ac_temp": { + "name": "Frostschutz Klima" + }, + "eco_ac_temp": { + "name": "Eco Klima" + }, + "comfort_ac_temp": { + "name": "Komfort Klima" + }, + "boost_ac_temp": { + "name": "Boost Klima" + }, + "frost_away_temp": { + "name": "Frostschutz Abwesend" + }, + "eco_away_temp": { + "name": "Eco Abwesend" + }, + "comfort_away_temp": { + "name": "Komfort Abwesend" + }, + "boost_away_temp": { + "name": "Boost Abwesend" + }, + "eco_ac_away_temp": { + "name": "Eco Klima Abwesend" + }, + "comfort_ac_away_temp": { + "name": "Komfort Klima Abwesend" + }, + "boost_ac_away_temp": { + "name": "Boost Klima Abwesend" + } + } + }, + "services": { + "set_auto_tpi_mode": { + "name": "Auto-TPI-Modus einstellen", + "description": "Aktiviert oder deaktiviert den Auto-TPI-Lernmodus.", + "fields": { + "auto_tpi_mode": { + "name": "Auto-TPI-Modus", + "description": "Auto-TPI-Lernen aktivieren (true) oder deaktivieren (false)" + }, + "reinitialise": { + "name": "Lern-Daten zurücksetzen", + "description": "Alle Lerndaten bei Aktivierung des Auto-TPI-Modus zurücksetzen (Standardwert: true)" + } + } + }, + "reset_auto_tpi_capacities": { + "name": "Auto-TPI-Funktionen zurücksetzen", + "description": "Setzt die maximalen Heiz- und Kühlkapazitäten auf 1,0 °C/h zurück. Die erlernten Koeffizienten werden NICHT zurückgesetzt." + }, + "auto_tpi_calibrate_capacity": { + "name": "Auto-TPI-Kapazitätskalibrierung", + "description": "Kalibriert die Heiz-/Kühlleistung (in °C/Stunde) mithilfe einer linearen Regression auf Basis der historischen Daten der Entity. Die erlernten TPI-Koeffizienten werden NICHT aktualisiert.", + "fields": { + "start_date": { + "name": "Startdatum des Verlaufs", + "description": "Datum, ab dem der Verlauf abgerufen werden soll (standardmäßig sind es 30 Tage)" + }, + "end_date": { + "name": "Enddatum des Verlaufs", + "description": "Datum, bis zu dem der Verlauf abgerufen werden soll (Standardwert: jetzt)" + }, + "hvac_mode": { + "name": "HVAC-Modus", + "description": "Welcher Modus soll kalibriert werden (Heizung oder Kühlung)?" + }, + "save_to_config": { + "name": "Konfiguration speichern", + "description": "Die berechnete Kapazität auf die Auto-TPI-Konfiguration anwenden" + }, + "min_power_threshold": { + "name": "Mindestleistungsschwelle", + "description": "Mindestprozentsatz der Leistung, ab dem ein Zyklus als gesättigt gilt (Standardwert 95 %). Niedrigere Werte umfassen mehr Zyklen, können jedoch weniger genau sein." + } + } + }, + "set_tpi_parameters": { + "name": "TPI-Parameter definieren", + "description": "TPI-Einstellungen ändern", + "fields": { + "minimal_activation_delay": { + "name": "Mindestaktivierungszeit", + "description": "Zeit in Sekunden, unterhalb derer das Gerät nicht aktiviert wird" + }, + "minimal_deactivation_delay": { + "name": "Mindestdeaktivierungszeit", + "description": "Zeit in Sekunden, nach deren Ablauf das Gerät aktiv bleibt" + }, + "tpi_threshold_low": { + "name": "Untere TPI-Schwelle", + "description": "Seuil en ° en dessous duquel le TPI sera activé. 0 signifie pas de seuil" + }, + "tpi_threshold_high": { + "name": "Obere TPI-Schwelle", + "description": "Schwellenwert in °, bei dessen Überschreitung der TPI-Algorithmus deaktiviert wird. 0 bedeutet kein Schwellenwert." + } + } + }, + "lock": { + "name": "Sperren", + "description": "Sperrt den Thermostat und verhindert so jegliche Änderung seiner Konfiguration über die Benutzeroberfläche oder Automatisierungen.", + "fields": { + "code": { + "name": "Sperrcode", + "description": "Der optionale Sperrcode" + } + } + }, + "unlock": { + "name": "Entsperren", + "description": "Entriegelt den Thermostat und ermöglicht so Änderungen an seiner Konfiguration.", + "fields": { + "code": { + "name": "Sperrcode", + "description": "Der optionale Sperrcode" + } + } + } + }, + "exceptions": { + "auto_tpi_learning_stopped": { + "message": "Das Auto-TPI-Lernen für {name} wurde angehalten, da drei aufeinanderfolgende Fehler festgestellt wurden. Grund: {reason}. Bitte überprüfen Sie Ihre Konfiguration." + } + } +} \ No newline at end of file diff --git a/custom_components/versatile_thermostat/translations/el.json b/custom_components/versatile_thermostat/translations/el.json new file mode 100644 index 0000000..0bffda9 --- /dev/null +++ b/custom_components/versatile_thermostat/translations/el.json @@ -0,0 +1,580 @@ +{ + "title": "Διαμόρφωση Ευέλικτου Θερμοστάτη", + "config": { + "flow_title": "Διαμόρφωση Ευέλικτου Θερμοστάτη", + "step": { + "user": { + "title": "Προσθήκη νέου Ευέλικτου Θερμοστάτη", + "description": "Κύρια υποχρεωτικά χαρακτηριστικά", + "data": { + "name": "Όνομα", + "thermostat_type": "Τύπος Θερμοστάτη", + "temperature_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα θερμοκρασίας", + "external_temperature_sensor_entity_id": "Ταυτότητα οντότητας εξωτερικού αισθητήρα θερμοκρασίας", + "cycle_min": "Διάρκεια κύκλου (λεπτά)", + "temp_min": "Ελάχιστη επιτρεπτή θερμοκρασία", + "temp_max": "Μέγιστη επιτρεπτή θερμοκρασία", + "device_power": "Ισχύς συσκευής", + "use_window_feature": "Χρήση ανίχνευσης παραθύρου", + "use_motion_feature": "Χρήση ανίχνευσης κίνησης", + "use_power_feature": "Χρήση διαχείρισης ισχύος", + "use_presence_feature": "Χρήση ανίχνευσης παρουσίας" + }, + "data_description": { + "cycle_min": "Minimum duration of one heating/cooling cycle in minutes to avoid too frequent switching", + "temp_min": "Minimum temperature allowed for the thermostat", + "temp_max": "Maximum temperature allowed for the thermostat", + "step_temperature": "Temperature step for setting the target temperature", + "temperature_sensor_entity_id": "Room temperature sensor entity id", + "last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor", + "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" + } + }, + "lock": { + "title": "Διαχείριση κλειδώματος", + "description": "Η λειτουργία κλειδώματος αποτρέπει αλλαγές στη διαμόρφωση ενός θερμοστάτη από το περιβάλλον χρήστη ή αυτοματισμούς, διατηρώντας παράλληλα τον θερμοστάτη σε λειτουργία.", + "data": { + "use_lock_central_config": "Χρήση κεντρικής διαμόρφωσης κλειδώματος", + "lock_code": "Ο κωδικός PIN για το ξεκλείδωμα του θερμοστάτη (4 ψηφία)", + "lock_users": "Το κλείδωμα θα αποτρέψει εντολές από χρήστες", + "lock_automations": "Το κλείδωμα θα αποτρέψει εντολές από αυτοματισμούς και ενσωματώσεις (π.χ. προγραμματιστής)" + }, + "data_description": { + "use_lock_central_config": "Επιλέξτε για χρήση της κεντρικής διαμόρφωσης κλειδώματος. Αποεπιλέξτε για χρήση συγκεκριμένης διαμόρφωσης κλειδώματος για αυτόν τον VTherm", + "lock_code": "Ο κωδικός PIN για το ξεκλείδωμα του θερμοστάτη (4 ψηφία)" + } + }, + "type": { + "title": "Συνδεδεμένες οντότητες", + "description": "Χαρακτηριστικά συνδεδεμένων οντοτήτων", + "data": { + "heater_entity_id": "1ος διακόπτης θερμαντήρα", + "heater_entity2_id": "2ος διακόπτης θερμαντήρα", + "heater_entity3_id": "3ος διακόπτης θερμαντήρα", + "heater_entity4_id": "4ος διακόπτης θερμαντήρα", + "proportional_function": "Αλγόριθμος", + "climate_entity_id": "1η υποκείμενη κλιματική οντότητα", + "climate_entity2_id": "2η υποκείμενη κλιματική οντότητα", + "climate_entity3_id": "3η υποκείμενη κλιματική οντότητα", + "climate_entity4_id": "4η υποκείμενη κλιματική οντότητα", + "ac_mode": "Λειτουργία AC", + "sync_device_internal_temp": "Συγχρονισμός εσωτερικής θερμοκρασίας συσκευής", + "valve_entity_id": "1ος αριθμός βαλβίδας", + "valve_entity2_id": "2ος αριθμός βαλβίδας", + "valve_entity3_id": "3ος αριθμός βαλβίδας", + "valve_entity4_id": "4ος αριθμός βαλβίδας", + "auto_regulation_mode": "Αυτόματη ρύθμιση", + "auto_regulation_dtemp": "Όριο ρύθμισης", + "auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης", + "inverse_switch_command": "Αντίστροφη εντολή διακόπτη", + "auto_fan_mode": "Auto fan mode" + }, + "data_description": { + "heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα", + "heater_entity2_id": "Προαιρετική 2η ταυτότητα οντότητας θερμαντήρα. Αφήστε κενό αν δεν χρησιμοποιείται", + "heater_entity3_id": "Προαιρετική 3η ταυτότητα οντότητας θερμαντήρα. Αφήστε κενό αν δεν χρησιμοποιείται", + "heater_entity4_id": "Προαιρετική 4η ταυτότητα οντότητας θερμαντήρα. Αφήστε κενό αν δεν χρησιμοποιείται", + "proportional_function": "Αλγόριθμος προς χρήση (άλλοι αλγόριθμοι μπορούν να εγκατασταθούν ως {plugins_link})", + "climate_entity_id": "Ταυτότητα υποκείμενης κλιματικής οντότητας", + "climate_entity2_id": "2η ταυτότητα υποκείμενης κλιματικής οντότητας", + "climate_entity3_id": "3η ταυτότητα υποκείμενης κλιματικής οντότητας", + "climate_entity4_id": "4η ταυτότητα υποκείμενης κλιματικής οντότητας", + "ac_mode": "Χρήση της λειτουργίας Κλιματισμού (AC)", + "sync_device_internal_temp": "Συγχρονισμός εσωτερικής θερμοκρασίας συσκευής", + "valve_entity_id": "1η ταυτότητα αριθμού βαλβίδας", + "valve_entity2_id": "2η ταυτότητα αριθμού βαλβίδας", + "valve_entity3_id": "3η ταυτότητα αριθμού βαλβίδας", + "valve_entity4_id": "4η ταυτότητα αριθμού βαλβίδας", + "auto_regulation_mode": "Αυτόματη προσαρμογή της στοχευμένης θερμοκρασίας", + "auto_regulation_dtemp": "Το όριο σε ° κάτω από το οποίο η αλλαγή θερμοκρασίας δεν θα αποστέλλεται", + "auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης", + "inverse_switch_command": "Για διακόπτη με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστρέψετε την εντολή", + "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary" + } + }, + "tpi": { + "title": "TPI", + "description": "Χαρακτηριστικά Χρονικά Αναλογικού Ολοκληρωτικού (TPI)", + "data": { + "tpi_coef_int": "Συντελεστής για χρήση στη διαφορά εσωτερικής θερμοκρασίας", + "tpi_coef_ext": "Συντελεστής για χρήση στη διαφορά εξωτερικής θερμοκρασίας" + } + }, + "presets": { + "title": "Προκαθορισμένα", + "description": "Για κάθε προκαθορισμένο, δώστε την επιθυμητή θερμοκρασία (0 για να αγνοηθεί το προκαθορισμένο)", + "data": { + "eco_temp": "Θερμοκρασία στο προκαθορισμένο Eco", + "comfort_temp": "Θερμοκρασία στο προκαθορισμένο Comfort", + "boost_temp": "Θερμοκρασία στο προκαθορισμένο Boost", + "frost_temp": "Θερμοκρασία στο προκαθορισμένο Frost protection", + "eco_ac_temp": "Θερμοκρασία στο προκαθορισμένο Eco για λειτουργία AC", + "comfort_ac_temp": "Θερμοκρασία στο προκαθορισμένο Comfort για λειτουργία AC", + "boost_ac_temp": "Θερμοκρασία στο προκαθορισμένο Boost για λειτουργία AC" + } + }, + "window": { + "title": "Διαχείριση Παραθύρων", + "description": "Ανοίξτε τη διαχείριση παραθύρων.\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται\nΜπορείτε επίσης να ρυθμίσετε αυτόματη ανίχνευση ανοίγματος παραθύρου με βάση τη μείωση της θερμοκρασίας", + "data": { + "window_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παραθύρου", + "window_delay": "Καθυστέρηση αισθητήρα παραθύρου (δευτερόλεπτα)", + "window_auto_open_threshold": "Κατώφλι μείωσης θερμοκρασίας για αυτόματη ανίχνευση ανοίγματος παραθύρου (σε °/λεπτό)", + "window_auto_close_threshold": "Κατώφλι αύξησης θερμοκρασίας για τέλος αυτόματης ανίχνευσης (σε °/λεπτό)", + "window_auto_max_duration": "Μέγιστη διάρκεια αυτόματης ανίχνευσης ανοίγματος παραθύρου (σε λεπτά)" + }, + "data_description": { + "window_sensor_entity_id": "Αφήστε κενό αν δεν πρέπει να χρησιμοποιηθεί αισθητήρας παραθύρου", + "window_delay": "Η καθυστέρηση σε δευτερόλεπτα πριν ληφθεί υπόψη η ανίχνευση του αισθητήρα", + "window_auto_open_threshold": "Συνιστώμενη τιμή: μεταξύ 0.05 και 0.1. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου", + "window_auto_close_threshold": "Συνιστώμενη τιμή: 0. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου", + "window_auto_max_duration": "Συνιστώμενη τιμή: 60 (μία ώρα). Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου" + } + }, + "motion": { + "title": "Διαχείριση Κίνησης", + "description": "Διαχείριση αισθητήρα κίνησης. Το προκαθορισμένο μπορεί να αλλάζει αυτόματα ανάλογα με ανίχνευση κίνησης\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.\nΟι επιλογές motion_preset και no_motion_preset πρέπει να οριστούν στο αντίστοιχο όνομα προκαθορισμένου", + "data": { + "motion_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα κίνησης", + "motion_delay": "Καθυστέρηση ενεργοποίησης", + "motion_off_delay": "Καθυστέρηση απενεργοποίησης", + "motion_preset": "Προκαθορισμένο κίνησης", + "no_motion_preset": "Προκαθορισμένο χωρίς κίνηση" + }, + "data_description": { + "motion_sensor_entity_id": "Η ταυτότητα οντότητας του αισθητήρα κίνησης", + "motion_delay": "Καθυστέρηση ενεργοποίησης κίνησης (δευτερόλεπτα)", + "motion_off_delay": "Καθυστέρηση απενεργοποίησης κίνησης (δευτερόλεπτα)", + "motion_preset": "Το προκαθορισμένο που θα χρησιμοποιηθεί όταν ανιχνευθεί κίνηση", + "no_motion_preset": "Το προκαθορισμένο που θα χρησιμοποιηθεί όταν δεν ανιχνευθεί κίνηση" + } + }, + "power": { + "title": "Διαχείριση Ενέργειας", + "description": "Χαρακτηριστικά διαχείρισης ενέργειας.\nΔίνει τον αισθητήρα ενέργειας και τον μέγιστο αισθητήρα ενέργειας του σπιτιού σας.\nΣτη συνέχεια καθορίστε την κατανάλωση ενέργειας του θερμαντήρα όταν είναι ενεργοποιημένος.\nΌλοι οι αισθητήρες και η ισχύς της συσκευής πρέπει να έχουν την ίδια μονάδα (kW ή W).\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.", + "data": { + "power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα ενέργειας", + "max_power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα μέγιστης ενέργειας", + "power_temp": "Θερμοκρασία για Αποβολή Ενέργειας" + } + }, + "presence": { + "title": "Διαχείριση Παρουσίας", + "description": "Χαρακτηριστικά διαχείρισης παρουσίας.\nΔίνει έναν αισθητήρα παρουσίας του σπιτιού σας (αληθές αν κάποιος είναι παρών).\nΣτη συνέχεια καθορίστε είτε το προκαθορισμένο που θα χρησιμοποιηθεί όταν ο αισθητήρας παρουσίας είναι ψευδής ή την απόκλιση στη θερμοκρασία που θα εφαρμοστεί.\nΑν δοθεί προκαθορισμένο, η απόκλιση δεν θα χρησιμοποιηθεί.\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.", + "data": { + "presence_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παρουσίας", + "eco_away_temp": "Θερμοκρασία στο προκαθορισμένο Eco όταν δεν υπάρχει παρουσία", + "comfort_away_temp": "Θερμοκρασία στο προκαθορισμένο Comfort όταν δεν υπάρχει παρουσία", + "boost_away_temp": "Θερμοκρασία στο προκαθορισμένο Boost όταν δεν υπάρχει παρουσία", + "frost_away_temp": "Θερμοκρασία στο προκαθορισμένο Frost protection όταν δεν υπάρχει παρουσία", + "eco_ac_away_temp": "Θερμοκρασία στο προκαθορισμένο Eco όταν δεν υπάρχει παρουσία σε λειτουργία AC", + "comfort_ac_away_temp": "Θερμοκρασία στο προκαθορισμένο Comfort όταν δεν υπάρχει παρουσία σε λειτουργία AC", + "boost_ac_away_temp": "Θερμοκρασία στο προκαθορισμένο Boost όταν δεν υπάρχει παρουσία σε λειτουργία AC" + } + }, + "advanced": { + "title": "Προχωρημένες Παράμετροι", + "description": "Διαμόρφωση των προχωρημένων παραμέτρων. Αφήστε τις προεπιλεγμένες τιμές αν δεν γνωρίζετε τι κάνετε.\nΑυτές οι παράμετροι μπορούν να οδηγήσουν σε πολύ κακή ρύθμιση θερμοκρασίας ή ενέργειας.", + "data": { + "minimal_activation_delay": "Ελάχιστη καθυστέρηση ενεργοποίησης", + "safety_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)", + "safety_min_on_percent": "Ελάχιστο ποσοστό ισχύος για ενεργοποίηση λειτουργίας ασφαλείας", + "safety_default_on_percent": "Ποσοστό ισχύος για χρήση σε λειτουργία ασφαλείας", + "repair_incorrect_state": "Επιδιόρθωση ακατάλληλης κατάστασης" + }, + "data_description": { + "minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία η συσκευή δεν θα ενεργοποιηθεί", + "safety_delay_min": "Μέγιστη επιτρεπτή καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πέρα από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας", + "safety_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για την ενεργοποίηση του προεπιλεγμένου ασφάλειας. Κάτω από αυτό το ποσοστό ισχύος το θερμοστάτη δεν θα πάει στο προεπιλεγμένο ασφάλειας.", + "safety_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφάλειας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφάλειας.", + "repair_incorrect_state": "Αυτόματη επιδιόρθωση της ακατάλληλης κατάστασης των υποκείμενων οντοτήτων όταν εντοπιστεί. Η λειτουργία θα ξαναστείλει την επιθυμητή εντολή εάν η κατάσταση της υποκείμενης συσκευής δεν ταιριάζει με την αναμενόμενη κατάσταση." + } + }, + "sync_device_internal_temp": { + "title": "Synchronize device internal temperature", + "description": "Configuration to synchronize the internal temperature of the underlying devices with selected entities", + "data": { + "sync_with_calibration": "Apply offset calibration", + "sync_entity_ids": "Entities id used for synchronization" + }, + "data_description": { + "sync_with_calibration": "Check to apply the offset calibration when synchronizing the internal temperature. Uncheck to directly copy the temperature from the selected entities", + "sync_entity_ids": "The list of the entities used to synchronize the internal temperature of the underlying devices. There should be one per underlying device. Should be a calibration entity if checkbox is selected or a temperature entity otherwise. Both are `number` entities." + } + }, + "valve_regulation": { + "title": "Αυτορύθμιση με βαλβίδα", + "description": "Διαμόρφωση για αυτορύθμιση με άμεσο έλεγχο της βαλβίδας", + "data": { + "offset_calibration_entity_ids": "Οντότητες βαθμονόμησης μετατόπισης", + "opening_degree_entity_ids": "Οντότητες βαθμού ανοίγματος", + "closing_degree_entity_ids": "Οντότητες βαθμού κλεισίματος", + "proportional_function": "Αλγόριθμος", + "opening_threshold_degree": "Κατώφλι ανοίγματος", + "min_opening_degrees": "Ελάχιστοι βαθμοί ανοίγματος", + "max_opening_degrees": "Μέγιστος βαθμός ανοίγματος", + "max_closing_degree": "Μέγιστος βαθμός κλεισίματος" + }, + "data_description": { + "offset_calibration_entity_ids": "Η λίστα των οντοτήτων 'βαθμονόμησης μετατόπισης'. Ορίστε το εάν το TRV σας διαθέτει την οντότητα για καλύτερη ρύθμιση. Θα πρέπει να υπάρχει ένα για κάθε υποκείμενη κλιματική οντότητα", + "opening_degree_entity_ids": "Η λίστα των οντοτήτων 'βαθμού ανοίγματος'. Θα πρέπει να υπάρχει ένα για κάθε υποκείμενη κλιματική οντότητα", + "closing_degree_entity_ids": "Η λίστα των οντοτήτων 'βαθμού κλεισίματος'. Ορίστε το εάν το TRV σας διαθέτει την οντότητα για καλύτερη ρύθμιση. Θα πρέπει να υπάρχει ένα για κάθε υποκείμενη κλιματική οντότητα", + "proportional_function": "Αλγόριθμος προς χρήση (άλλοι αλγόριθμοι μπορούν να εγκατασταθούν ως {plugins_link})", + "opening_threshold_degree": "Τιμή ανοίγματος της βαλβίδας κάτω από την οποία η βαλβίδα πρέπει να θεωρείται κλειστή (και τότε θα ισχύει το 'max_closing_degree')", + "min_opening_degrees": "Ελάχιστη τιμή βαθμού ανοίγματος για κάθε υποκείμενη συσκευή, διαχωρισμένη με κόμμα. Προεπιλεγμένη τιμή 0. Παράδειγμα: 20, 25, 30", + "max_opening_degrees": "Μέγιστη τιμή του βαθμού ανοίγματος. Η βαλβίδα δεν θα ανοίξει ποτέ περισσότερο από αυτήν την τιμή", + "max_closing_degree": "Μέγιστη τιμή του βαθμού κλεισίματος. Η βαλβίδα δεν θα κλείσει ποτέ περισσότερο από αυτήν την τιμή. Ορίστε το στο 100 για πλήρες κλείσιμο της βαλβίδας" + } + }, + "heating_failure_detection": { + "title": "Heating failure detection", + "description": "Configure the heating failure detection feature. This detects anomalies when heating is expected but temperature doesn't increase, or when heating is off but temperature keeps rising.", + "data": { + "use_heating_failure_detection_feature": "Enable heating failure detection", + "use_heating_failure_detection_central_config": "Use central heating failure detection configuration", + "heating_failure_threshold": "Heating failure threshold", + "cooling_failure_threshold": "Cooling failure threshold", + "heating_failure_detection_delay": "Detection delay (minutes)", + "temperature_change_tolerance": "Temperature change tolerance (°C)", + "failure_detection_enable_template": "Detection enable template" + }, + "data_description": { + "use_heating_failure_detection_feature": "Enable the detection of heating anomalies for VTherms with TPI", + "use_heating_failure_detection_central_config": "Check to use the central heating failure detection configuration. Uncheck to use specific parameters for this VTherm", + "heating_failure_threshold": "On percent threshold above which heating should cause temperature increase (0.9 = 90%)", + "cooling_failure_threshold": "On percent threshold below which temperature should not increase (0.0 = 0%)", + "heating_failure_detection_delay": "Time in minutes to wait before checking if temperature has changed as expected", + "temperature_change_tolerance": "Minimum temperature change in degrees to consider significant. Smaller changes are ignored to filter sensor noise (default: 0.5°C)", + "failure_detection_enable_template": "An optional Jinja2 template that must return True to enable detection. Useful to temporarily disable detection when an external heat source is active (e.g., `is_state('binary_sensor.wood_stove', 'off')` in double curly braces). If empty, detection is always enabled." + } + } + }, + "error": { + "unknown": "Απροσδόκητο λάθος", + "unknown_entity": "Άγνωστο αναγνωριστικό οντότητας", + "window_open_detection_method": "Πρέπει να χρησιμοποιηθεί μόνο μία μέθοδος ανίχνευσης ανοιχτού παραθύρου. Χρησιμοποιήστε αισθητήρα ή αυτόματη ανίχνευση μέσω κατωφλίου θερμοκρασίας αλλά όχι και τα δύο", + "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", + "valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings", + "sync_device_internal_temp_nb_entities_incorrect": "The number of entities for synchronize device internal temperature should be equal to the number of underlyings", + "min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30", + "max_opening_degrees_format": "Αναμένεται μια λίστα θετικών ακεραίων μεταξύ 0 και 100 χωρισμένη με κόμματα. Παράδειγμα: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Για κάθε υποκείμενη συσκευή, το max_opening_degrees πρέπει να είναι αυστηρά μεγαλύτερο από το min_opening_degrees. Ελέγξτε τη διαμόρφωσή σας.", + "vswitch_configuration_incorrect": "The command customization configuration is incorrect. It is required for underlying entities that are not switches, and the format must be service_name[/attribute:value]. More information is available in the README" + }, + "abort": { + "already_configured": "Η συσκευή έχει ήδη ρυθμιστεί" + } + }, + "options": { + "flow_title": "Διαμόρφωση Ευέλικτου Θερμοστάτη", + "step": { + "user": { + "title": "Προσθήκη νέου Ευέλικτου Θερμοστάτη", + "description": "Κύρια υποχρεωτικά χαρακτηριστικά", + "data": { + "name": "Όνομα", + "thermostat_type": "Τύπος θερμοστάτη", + "temperature_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα θερμοκρασίας", + "external_temperature_sensor_entity_id": "Ταυτότητα οντότητας εξωτερικού αισθητήρα θερμοκρασίας", + "cycle_min": "Διάρκεια κύκλου (λεπτά)", + "temp_min": "Ελάχιστη επιτρεπόμενη θερμοκρασία", + "temp_max": "Μέγιστη επιτρεπόμενη θερμοκρασία", + "device_power": "Ισχύς συσκευής (kW)", + "use_window_feature": "Χρήση ανίχνευσης παραθύρου", + "use_motion_feature": "Χρήση ανίχνευσης κίνησης", + "use_power_feature": "Χρήση διαχείρισης ενέργειας", + "use_presence_feature": "Χρήση ανίχνευσης παρουσίας" + }, + "data_description": { + "cycle_min": "Minimum duration of one heating/cooling cycle in minutes to avoid too frequent switching", + "temp_min": "Minimum temperature allowed for the thermostat", + "temp_max": "Maximum temperature allowed for the thermostat", + "step_temperature": "Temperature step for setting the target temperature", + "temperature_sensor_entity_id": "Room temperature sensor entity id", + "last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor", + "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" + } + }, + "type": { + "title": "Συνδεδεμένες οντότητες", + "description": "Χαρακτηριστικά συνδεδεμένων οντοτήτων", + "data": { + "heater_entity_id": "1ος διακόπτης θερμαντήρα", + "heater_entity2_id": "2ος διακόπτης θερμαντήρα", + "heater_entity3_id": "3ος διακόπτης θερμαντήρα", + "heater_entity4_id": "4ος διακόπτης θερμαντήρα", + "proportional_function": "Αλγόριθμος", + "climate_entity_id": "1η υποκείμενη κλιματική οντότητα", + "climate_entity2_id": "2η υποκείμενη κλιματική οντότητα", + "climate_entity3_id": "3η υποκείμενη κλιματική οντότητα", + "climate_entity4_id": "4η υποκείμενη κλιματική οντότητα", + "ac_mode": "Λειτουργία AC", + "sync_device_internal_temp": "Συγχρονισμός εσωτερικής θερμοκρασίας συσκευής", + "valve_entity_id": "1ος αριθμός βαλβίδας", + "valve_entity2_id": "2ος αριθμός βαλβίδας", + "valve_entity3_id": "3ος αριθμός βαλβίδας", + "valve_entity4_id": "4ος αριθμός βαλβίδας", + "auto_regulation_mode": "Αυτορύθμιση", + "auto_regulation_dtemp": "Όριο ρύθμισης", + "auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης", + "inverse_switch_command": "Αντίστροφη εντολή διακόπτη", + "auto_fan_mode": "Auto fan mode" + }, + "data_description": { + "heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα", + "heater_entity2_id": "Προαιρετική ταυτότητα οντότητας 2ου θερμαντήρα. Αφήστε το κενό αν δεν χρησιμοποιείται", + "heater_entity3_id": "Προαιρετική ταυτότητα οντότητας 3ου θερμαντήρα. Αφήστε το κενό αν δεν χρησιμοποιείται", + "heater_entity4_id": "Προαιρετική ταυτότητα οντότητας 4ου θερμαντήρα. Αφήστε το κενό αν δεν χρησιμοποιείται", + "proportional_function": "Αλγόριθμος προς χρήση (άλλοι αλγόριθμοι μπορούν να εγκατασταθούν ως {plugins_link})", + "climate_entity_id": "Ταυτότητα οντότητας υποκείμενου κλίματος", + "climate_entity2_id": "Ταυτότητα οντότητας 2ου υποκείμενου κλίματος", + "climate_entity3_id": "Ταυτότητα οντότητας 3ου υποκείμενου κλίματος", + "climate_entity4_id": "Ταυτότητα οντότητας 4ου υποκείμενου κλίματος", + "ac_mode": "Χρήση της λειτουργίας Κλιματισμού (AC)", + "sync_device_internal_temp": "Συγχρονισμός εσωτερικής θερμοκρασίας συσκευής", + "valve_entity_id": "Ταυτότητα οντότητας 1ης βαλβίδας", + "valve_entity2_id": "Ταυτότητα οντότητας 2ης βαλβίδας", + "valve_entity3_id": "Ταυτότητα οντότητας 3ης βαλβίδας", + "valve_entity4_id": "Ταυτότητα οντότητας 4ης βαλβίδας", + "auto_regulation_mode": "Αυτόματη ρύθμιση της στοχευόμενης θερμοκρασίας", + "auto_regulation_dtemp": "Το κατώφλι σε °C κάτω από το οποίο η αλλαγή της θερμοκρασίας δεν θα αποστέλλεται", + "auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης", + "inverse_switch_command": "Για διακόπτες με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστραφεί η εντολή", + "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary" + } + }, + "tpi": { + "title": "TPI", + "description": "Χαρακτηριστικά Χρονικού Αναλογικού Ολοκληρωτικού (TPI)", + "data": { + "tpi_coef_int": "Συντελεστής που θα χρησιμοποιηθεί για την εσωτερική διαφορά θερμοκρασίας", + "tpi_coef_ext": "Συντελεστής που θα χρησιμοποιηθεί για την εξωτερική διαφορά θερμοκρασίας" + } + }, + "presets": { + "title": "Προεπιλογές", + "description": "Για κάθε προεπιλογή, δώστε τη στοχευόμενη θερμοκρασία (0 για να αγνοηθεί η προεπιλογή)", + "data": { + "eco_temp": "Θερμοκρασία στην οικονομική προεπιλογή", + "comfort_temp": "Θερμοκρασία στην άνετη προεπιλογή", + "boost_temp": "Θερμοκρασία στην ενισχυμένη προεπιλογή", + "frost_temp": "Θερμοκρασία στο προκαθορισμένο Frost protection", + "eco_ac_temp": "Θερμοκρασία στην οικονομική προεπιλογή για τη λειτουργία AC", + "comfort_ac_temp": "Θερμοκρασία στην άνετη προεπιλογή για τη λειτουργία AC", + "boost_ac_temp": "Θερμοκρασία στην ενισχυμένη προεπιλογή για τη λειτουργία AC" + } + }, + "window": { + "title": "Διαχείριση παραθύρου", + "description": "Διαχείριση ανοιχτού παραθύρου.\nΑφήστε την αντίστοιχη ταυτότητα οντότητας κενή αν δεν χρησιμοποιείται\nΜπορείτε επίσης να διαμορφώσετε την αυτόματη ανίχνευση ανοίγματος παραθύρου βάσει της μείωσης της θερμοκρασίας", + "data": { + "window_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παραθύρου", + "window_delay": "Καθυστέρηση αισθητήρα παραθύρου (δευτερόλεπτα)", + "window_auto_open_threshold": "Όριο μείωσης θερμοκρασίας για αυτόματη ανίχνευση ανοίγματος παραθύρου (σε °/λεπτό)", + "window_auto_close_threshold": "Όριο αύξησης θερμοκρασίας για τέλος αυτόματης ανίχνευσης (σε °/λεπτό)", + "window_auto_max_duration": "Μέγιστη διάρκεια αυτόματης ανίχνευσης ανοίγματος παραθύρου (σε λεπτά)" + }, + "data_description": { + "window_sensor_entity_id": "Αφήστε κενό αν δεν πρέπει να χρησιμοποιηθεί αισθητήρας παραθύρου", + "window_delay": "Η καθυστέρηση σε δευτερόλεπτα πριν ληφθεί υπόψη η ανίχνευση αισθητήρα", + "window_auto_open_threshold": "Συνιστώμενη τιμή: μεταξύ 0.05 και 0.1. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου", + "window_auto_close_threshold": "Συνιστώμενη τιμή: 0. Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου", + "window_auto_max_duration": "Συνιστώμενη τιμή: 60 (μία ώρα). Αφήστε κενό αν δεν χρησιμοποιείται αυτόματη ανίχνευση ανοίγματος παραθύρου" + } + }, + "motion": { + "title": "Διαχείριση κίνησης", + "description": "Διαχείριση αισθητήρα κίνησης. Ο προεπιλεγμένος τρόπος μπορεί να αλλάξει αυτόματα ανάλογα με την ανίχνευση κίνησης\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται.\nΤα motion_preset και no_motion_preset πρέπει να οριστούν στο αντίστοιχο όνομα προεπιλογής", + "data": { + "motion_sensor_entity_id": "Ανιχνευτής κίνησης entity id", + "motion_delay": "Καθυστέρηση ενεργοποίησης", + "motion_off_delay": "Καθυστέρηση απενεργοποίησης", + "motion_preset": "Προεπιλογή κίνησης", + "no_motion_preset": "Προεπιλογή χωρίς κίνηση" + }, + "data_description": { + "motion_sensor_entity_id": "Το entity id του ανιχνευτή κίνησης", + "motion_delay": "Καθυστέρηση ενεργοποίησης κίνησης (δευτερόλεπτα)", + "motion_off_delay": "Καθυστέρηση απενεργοποίησης κίνησης (δευτερόλεπτα)", + "motion_preset": "Η προεπιλογή που χρησιμοποιείται όταν ανιχνεύεται κίνηση", + "no_motion_preset": "Η προεπιλογή που χρησιμοποιείται όταν δεν ανιχνεύεται κίνηση" + } + }, + "power": { + "title": "Διαχείριση Ενέργειας", + "description": "Χαρακτηριστικά διαχείρισης ενέργειας.\nΠαρέχει τον αισθητήρα ισχύος και τον μέγιστο αισθητήρα ισχύος του σπιτιού σας.\nΣτη συνέχεια καθορίστε την κατανάλωση ενέργειας του θερμαντήρα όταν είναι ενεργοποιημένος.\nΌλοι οι αισθητήρες και η ισχύς της συσκευής πρέπει να έχουν την ίδια μονάδα (kW ή W).\nΑφήστε το αντίστοιχο entity_id κενό εάν δεν χρησιμοποιείται.", + "data": { + "power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα ισχύος", + "max_power_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα μέγιστης ισχύος", + "power_temp": "Θερμοκρασία για Μείωση Ισχύος" + } + }, + "presence": { + "title": "Διαχείριση Παρουσίας", + "description": "Χαρακτηριστικά διαχείρισης παρουσίας.\nΠαρέχει έναν αισθητήρα παρουσίας του σπιτιού σας (αληθές εάν κάποιος είναι παρών).\nΣτη συνέχεια καθορίστε είτε το προεπιλεγμένο πρόγραμμα που θα χρησιμοποιηθεί όταν ο αισθητήρας παρουσίας είναι ψευδής είτε την θερμοκρασιακή διαφορά που θα εφαρμοστεί.\nΕάν δίνεται προεπιλογή, η διαφορά δεν θα χρησιμοποιηθεί.\nΑφήστε το αντίστοιχο entity_id κενό εάν δεν χρησιμοποιείται.", + "data": { + "presence_sensor_entity_id": "Ταυτότητα οντότητας αισθητήρα παρουσίας (αληθές είναι παρών)", + "eco_away_temp": "Θερμοκρασία στο πρόγραμμα Eco όταν δεν υπάρχει παρουσία", + "comfort_away_temp": "Θερμοκρασία στο πρόγραμμα Comfort όταν δεν υπάρχει παρουσία", + "boost_away_temp": "Θερμοκρασία στο πρόγραμμα Boost όταν δεν υπάρχει παρουσία", + "frost_away_temp": "Θερμοκρασία στο προκαθορισμένο Frost protection όταν δεν υπάρχει παρουσία", + "eco_ac_away_temp": "Θερμοκρασία στο πρόγραμμα Eco όταν δεν υπάρχει παρουσία σε λειτουργία AC", + "comfort_ac_away_temp": "Θερμοκρασία στο πρόγραμμα Comfort όταν δεν υπάρχει παρουσία σε λειτουργία AC", + "boost_ac_away_temp": "Θερμοκρασία στο πρόγραμμα Boost όταν δεν υπάρχει παρουσία σε λειτουργία AC" + } + }, + "advanced": { + "title": "Προηγμένες Παράμετροι", + "description": "Διαμόρφωση των προηγμένων παραμέτρων. Αφήστε τις προεπιλεγμένες τιμές εάν δεν γνωρίζετε τι κάνετε.\nΑυτές οι παράμετροι μπορούν να οδηγήσουν σε πολύ κακή ρύθμιση θερμοκρασίας ή ενέργειας.", + "data": { + "minimal_activation_delay": "Ελάχιστη καθυστέρηση ενεργοποίησης", + "safety_delay_min": "Καθυστέρηση ασφαλείας (σε λεπτά)", + "safety_min_on_percent": "Ελάχιστο ποσοστό ισχύος για τη λειτουργία ασφαλείας", + "safety_default_on_percent": "Ποσοστό ισχύος που θα χρησιμοποιηθεί στη λειτουργία ασφαλείας", + "repair_incorrect_state": "Repair incorrect state", + "use_advanced_central_config": "Use central advanced configuration" + }, + "data_description": { + "minimal_activation_delay": "Καθυστέρηση σε δευτερόλεπτα κάτω από την οποία ο εξοπλισμός δεν θα ενεργοποιηθεί", + "safety_delay_min": "Μέγιστη επιτρεπόμενη καθυστέρηση σε λεπτά μεταξύ δύο μετρήσεων θερμοκρασίας. Πάνω από αυτή την καθυστέρηση, ο θερμοστάτης θα μεταβεί σε κατάσταση ασφαλείας", + "safety_min_on_percent": "Ελάχιστη τιμή ποσοστού θέρμανσης για ενεργοποίηση του προεπιλεγμένου ασφαλείας. Κάτω από αυτό το ποσοστό ισχύος, ο θερμοστάτης δεν θα μεταβεί στο προεπιλεγμένο ασφαλείας", + "safety_default_on_percent": "Η προεπιλεγμένη τιμή ποσοστού ισχύος θέρμανσης στο προεπιλεγμένο ασφαλείας. Ορίστε σε 0 για να απενεργοποιήσετε τη θερμάστρα στο παρόν ασφαλείας", + "repair_incorrect_state": "Automatically repair incorrect state of underlying entities when detected. The feature will re-send the desired command if the underlying device state doesn't match the expected state.", + "use_advanced_central_config": "Check to use the central advanced configuration. Uncheck to use a specific advanced configuration for this VTherm" + } + }, + "lock": { + "title": "Διαχείριση κλειδώματος", + "description": "Η λειτουργία κλειδώματος αποτρέπει αλλαγές στη διαμόρφωση ενός θερμοστάτη από το περιβάλλον χρήστη ή αυτοματισμούς, διατηρώντας παράλληλα τον θερμοστάτη σε λειτουργία.", + "data": { + "use_lock_central_config": "Χρήση κεντρικής διαμόρφωσης κλειδώματος", + "lock_code": "Ο κωδικός PIN για το ξεκλείδωμα του θερμοστάτη (4 ψηφία)", + "lock_users": "Το κλείδωμα θα αποτρέψει εντολές από χρήστες", + "lock_automations": "Το κλείδωμα θα αποτρέψει εντολές από αυτοματισμούς και ενσωματώσεις (π.χ. προγραμματιστής)" + }, + "data_description": { + "use_lock_central_config": "Επιλέξτε για χρήση της κεντρικής διαμόρφωσης κλειδώματος. Αποεπιλέξτε για χρήση συγκεκριμένης διαμόρφωσης κλειδώματος για αυτόν τον VTherm", + "lock_code": "Ο κωδικός PIN για το ξεκλείδωμα του θερμοστάτη (4 ψηφία)" + } + }, + "sync_device_internal_temp": { + "title": "Synchronize device internal temperature", + "description": "Configuration to synchronize the internal temperature of the underlying devices with selected entities", + "data": { + "sync_with_calibration": "Apply offset calibration", + "sync_entity_ids": "Entities id used for synchronization" + }, + "data_description": { + "sync_with_calibration": "Check to apply the offset calibration when synchronizing the internal temperature. Uncheck to directly copy the temperature from the selected entities", + "sync_entity_ids": "The list of the entities used to synchronize the internal temperature of the underlying devices. There should be one per underlying device. Should be a calibration entity if checkbox is selected or a temperature entity otherwise. Both are `number` entities." + } + }, + "valve_regulation": { + "title": "Αυτορύθμιση με βαλβίδα - {name}", + "description": "Διαμόρφωση για αυτορύθμιση με άμεσο έλεγχο της βαλβίδας", + "data": { + "offset_calibration_entity_ids": "Οντότητες βαθμονόμησης μετατόπισης", + "opening_degree_entity_ids": "Οντότητες βαθμού ανοίγματος", + "closing_degree_entity_ids": "Οντότητες βαθμού κλεισίματος", + "proportional_function": "Αλγόριθμος", + "opening_threshold_degree": "Κατώφλι ανοίγματος", + "min_opening_degrees": "Ελάχιστοι βαθμοί ανοίγματος", + "max_opening_degrees": "Μέγιστος βαθμός ανοίγματος", + "max_closing_degree": "Μέγιστος βαθμός κλεισίματος" + }, + "data_description": { + "offset_calibration_entity_ids": "Η λίστα των οντοτήτων 'βαθμονόμησης μετατόπισης'. Ορίστε το εάν το TRV σας διαθέτει την οντότητα για καλύτερη ρύθμιση. Θα πρέπει να υπάρχει ένα για κάθε υποκείμενη κλιματική οντότητα", + "opening_degree_entity_ids": "Η λίστα των οντοτήτων 'βαθμού ανοίγματος'. Θα πρέπει να υπάρχει ένα για κάθε υποκείμενη κλιματική οντότητα", + "closing_degree_entity_ids": "Η λίστα των οντοτήτων 'βαθμού κλεισίματος'. Ορίστε το εάν το TRV σας διαθέτει την οντότητα για καλύτερη ρύθμιση. Θα πρέπει να υπάρχει ένα για κάθε υποκείμενη κλιματική οντότητα", + "proportional_function": "Αλγόριθμος προς χρήση (άλλοι αλγόριθμοι μπορούν να εγκατασταθούν ως {plugins_link})", + "opening_threshold_degree": "Τιμή ανοίγματος της βαλβίδας κάτω από την οποία η βαλβίδα πρέπει να θεωρείται κλειστή (και τότε θα ισχύει το 'max_closing_degree')", + "min_opening_degrees": "Ελάχιστη τιμή βαθμού ανοίγματος για κάθε υποκείμενη συσκευή, διαχωρισμένη με κόμμα. Προεπιλεγμένη τιμή 0. Παράδειγμα: 20, 25, 30", + "max_opening_degrees": "Μέγιστη τιμή του βαθμού ανοίγματος. Η βαλβίδα δεν θα ανοίξει ποτέ περισσότερο από αυτήν την τιμή", + "max_closing_degree": "Μέγιστη τιμή του βαθμού κλεισίματος. Η βαλβίδα δεν θα κλείσει ποτέ περισσότερο από αυτήν την τιμή. Ορίστε το στο 100 για πλήρες κλείσιμο της βαλβίδας" + } + }, + "heating_failure_detection": { + "title": "Heating failure detection", + "description": "Configure the heating failure detection feature. This detects anomalies when heating is expected but temperature doesn't increase, or when heating is off but temperature keeps rising.", + "data": { + "use_heating_failure_detection_feature": "Enable heating failure detection", + "use_heating_failure_detection_central_config": "Use central heating failure detection configuration", + "heating_failure_threshold": "Heating failure threshold", + "cooling_failure_threshold": "Cooling failure threshold", + "heating_failure_detection_delay": "Detection delay (minutes)", + "temperature_change_tolerance": "Temperature change tolerance (°C)", + "failure_detection_enable_template": "Detection enable template" + }, + "data_description": { + "use_heating_failure_detection_feature": "Enable the detection of heating anomalies for VTherms with TPI", + "use_heating_failure_detection_central_config": "Check to use the central heating failure detection configuration. Uncheck to use specific parameters for this VTherm", + "heating_failure_threshold": "On percent threshold above which heating should cause temperature increase (0.9 = 90%)", + "cooling_failure_threshold": "On percent threshold below which temperature should not increase (0.0 = 0%)", + "heating_failure_detection_delay": "Time in minutes to wait before checking if temperature has changed as expected", + "temperature_change_tolerance": "Minimum temperature change in degrees to consider significant. Smaller changes are ignored to filter sensor noise (default: 0.5°C)", + "failure_detection_enable_template": "An optional Jinja2 template that must return True to enable detection. Useful to temporarily disable detection when an external heat source is active (e.g., `is_state('binary_sensor.wood_stove', 'off')` in double curly braces). If empty, detection is always enabled." + } + } + }, + "error": { + "unknown": "Απροσδόκητο λάθος", + "unknown_entity": "Άγνωστο αναγνωριστικό οντότητας", + "window_open_detection_method": "Πρέπει να χρησιμοποιηθεί μόνο μία μέθοδος ανίχνευσης ανοιχτού παραθύρου. Χρησιμοποιήστε αισθητήρα ή αυτόματη ανίχνευση μέσω κατωφλίου θερμοκρασίας αλλά όχι και τα δύο", + "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", + "valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings", + "sync_device_internal_temp_nb_entities_incorrect": "The number of entities for synchronize device internal temperature should be equal to the number of underlyings", + "min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30", + "max_opening_degrees_format": "Αναμένεται μια λίστα θετικών ακεραίων μεταξύ 0 και 100 χωρισμένη με κόμματα. Παράδειγμα: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Για κάθε υποκείμενη συσκευή, το max_opening_degrees πρέπει να είναι αυστηρά μεγαλύτερο από το min_opening_degrees. Ελέγξτε τη διαμόρφωσή σας.", + "vswitch_configuration_incorrect": "The command customization configuration is incorrect. It is required for underlying entities that are not switches, and the format must be service_name[/attribute:value]. More information is available in the README" + }, + "abort": { + "already_configured": "Η συσκευή έχει ήδη ρυθμιστεί" + } + }, + "selector": { + "thermostat_type": { + "options": { + "thermostat_over_switch": "Θερμοστάτης πάνω σε διακόπτη", + "thermostat_over_climate": "Θερμοστάτης πάνω σε κλίμα", + "thermostat_over_valve": "Θερμοστάτης πάνω σε βαλβίδα" + } + }, + "auto_regulation_mode": { + "options": { + "auto_regulation_slow": "Αργή", + "auto_regulation_strong": "Δυνατή", + "auto_regulation_medium": "Μέτρια", + "auto_regulation_light": "Ελαφριά", + "auto_regulation_expert": "Εμπειρογνώμων", + "auto_regulation_none": "Χωρίς αυτόματη ρύθμιση" + } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "No auto fan", + "auto_fan_low": "Low", + "auto_fan_medium": "Medium", + "auto_fan_high": "High", + "auto_fan_turbo": "Turbo" + } + } + }, + "entity": { + "climate": { + "versatile_thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "power": "Μείωση", + "security": "Ασφάλεια", + "none": "Χειροκίνητο" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/versatile_thermostat/translations/en.json b/custom_components/versatile_thermostat/translations/en.json new file mode 100644 index 0000000..2f1fdfc --- /dev/null +++ b/custom_components/versatile_thermostat/translations/en.json @@ -0,0 +1,1049 @@ +{ + "title": "Versatile Thermostat configuration", + "config": { + "flow_title": "Versatile Thermostat configuration", + "step": { + "user": { + "title": "Type of Versatile Thermostat", + "description": "Choose thermostat type you want to create", + "data": { + "thermostat_type": "Thermostat type" + }, + "data_description": { + "thermostat_type": "Only one central configuration type is possible" + } + }, + "menu": { + "title": "Menu", + "description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.", + "menu_options": { + "main": "Main attributes", + "central_boiler": "Central boiler", + "type": "Underlyings", + "tpi": "TPI parameters", + "features": "Features", + "presets": "Presets", + "window": "Window detection", + "motion": "Motion detection", + "power": "Power management", + "presence": "Presence detection", + "advanced": "Advanced parameters", + "auto_start_stop": "Auto start and stop", + "valve_regulation": "Valve regulation configuration", + "sync_device_internal_temp": "Synchronize device temperature", + "lock": "Lock", + "heating_failure_detection": "Heating failure detection", + "auto_tpi_configuration": "Auto TPI - Configuration", + "auto_tpi_avg_settings": "Auto TPI - Weighted Average", + "auto_tpi_ema_settings": "Auto TPI - EWMA", + "finalize": "All done", + "configuration_not_complete": "Configuration not complete" + } + }, + "lock": { + "title": "Lock management", + "description": "The Lock feature prevents changes to a thermostat's configuration from the UI or automations while keeping the thermostat operational.", + "data": { + "use_lock_central_config": "Use central lock configuration", + "lock_code": "Optional pincode (4 digits)", + "lock_users": "Lock will prevent commands from Users", + "lock_automations": "Lock will prevent commands from Automations & integrations (i.e scheduler)", + "auto_relock_sec": "Auto-relock delay" + }, + "data_description": { + "use_lock_central_config": "Check to use the central lock configuration. Uncheck to use a specific lock configuration for this VTherm", + "auto_relock_sec": "Delay in seconds before automatically re-locking after an unlock. Set to 0 to disable auto-relock. Default is 30 seconds." + } + }, + "main": { + "title": "Add new Versatile Thermostat", + "description": "Main mandatory attributes", + "data": { + "name": "Name", + "thermostat_type": "Thermostat type", + "temperature_sensor_entity_id": "Room temperature", + "last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime", + "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", + "cycle_min": "Cycle duration (minutes)", + "temp_min": "Minimum temperature allowed", + "temp_max": "Maximum temperature allowed", + "step_temperature": "Temperature step", + "device_power": "Device power", + "use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.", + "use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).", + "used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler" + }, + "data_description": { + "cycle_min": "Minimum duration of one heating/cooling cycle in minutes to avoid too frequent switching", + "temp_min": "Minimum temperature allowed for the thermostat", + "temp_max": "Maximum temperature allowed for the thermostat", + "step_temperature": "Temperature step for setting the target temperature", + "temperature_sensor_entity_id": "Room temperature sensor entity id", + "last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor", + "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" + } + }, + "features": { + "title": "Features", + "description": "Thermostat features", + "data": { + "use_window_feature": "Use window detection", + "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 selecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page", + "use_auto_start_stop_feature": "Use the auto start and stop feature", + "use_heating_failure_detection_feature": "Use heating failure detection" + } + }, + "type": { + "title": "Linked entities", + "description": "Linked entities attributes", + "data": { + "underlying_entity_ids": "The device(s) to be controlled", + "heater_keep_alive": "Switch keep-alive interval in seconds", + "proportional_function": "Algorithm", + "ac_mode": "AC mode", + "sync_device_internal_temp": "Synchronize device internal temperature", + "auto_regulation_mode": "Self-regulation", + "auto_regulation_dtemp": "Regulation threshold", + "auto_regulation_periode_min": "Regulation minimum period", + "auto_regulation_use_device_temp": "Use internal temperature of the underlying", + "inverse_switch_command": "Inverse switch command", + "auto_fan_mode": "Auto fan mode", + "on_command_text": "Turn on command customization", + "vswitch_on_command": "Optional turn on commands", + "off_command_text": "Turn off command customization", + "vswitch_off_command": "Optional turn off commands" + }, + "data_description": { + "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 (other algorithms can be installed as {plugins_link})", + "ac_mode": "Use the Air Conditioning (AC) mode", + "sync_device_internal_temp": "Check to synchronize the internal temperature of the underlying device (fill the new menu item 'Synchronize device temperature' to configure it)", + "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", + "auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation (not needed in most cases)", + "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", + "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary", + "on_command_text": "For underlying of type `select` or `climate` you have to customize the commands." + } + }, + "tpi": { + "title": "TPI", + "description": "Time Proportional Integral attributes", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "tpi_threshold_low": "Low TPI threshold", + "tpi_threshold_high": "High TPI threshold", + "use_tpi_central_config": "Use central TPI configuration", + "minimal_activation_delay": "Minimum activation delay", + "minimal_deactivation_delay": "Minimum deactivation delay", + "auto_tpi_mode": "Enable Auto TPI Learning" + }, + "data_description": { + "tpi_coef_int": "Coefficient to use for internal temperature delta", + "tpi_coef_ext": "Coefficient to use for external temperature delta", + "tpi_threshold_low": "Threshold in ° under which the TPI algorithm will be on. 0 means no threshold", + "tpi_threshold_high": "Threshold in ° above which the TPI algorithm will be off. 0 means no threshold", + "use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm", + "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", + "minimal_deactivation_delay": "Delay in seconds under which the equipment will be kept active", + "auto_tpi_mode": "TPI learning session are started manually, read the doc first." + } + }, + "presets": { + "title": "Presets", + "description": "Select if the thermostat will use central preset - deselect for the thermostat to have its own presets", + "data": { + "use_presets_central_config": "Use central presets configuration" + } + }, + "window": { + "title": "Window management", + "description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease", + "data": { + "window_sensor_entity_id": "Window sensor entity id", + "window_delay": "Window sensor 'on' delay (seconds)", + "window_off_delay": "Window sensor 'off' delay (seconds)", + "window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)", + "window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)", + "window_auto_max_duration": "Maximum duration of automatic window open detection (in min)", + "use_window_central_config": "Use central window configuration", + "window_action": "Action" + }, + "data_description": { + "window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection", + "window_delay": "The delay in seconds before sensor 'on' detection is taken into account", + "window_off_delay": "The delay in seconds before sensor 'off' detection is taken into account. Leave it empty to use the same value as window on delay", + "window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used", + "window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used", + "window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used", + "use_window_central_config": "Select to use the central window configuration. Deselect to use a specific window configuration for this VTherm", + "window_action": "Action to perform if window is deteted as open" + } + }, + "motion": { + "title": "Motion management", + "description": "Motion sensor management. Preset can switch automatically depending on motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name", + "data": { + "motion_sensor_entity_id": "Motion sensor entity id", + "motion_delay": "Activation delay", + "motion_off_delay": "Deactivation delay", + "motion_preset": "Motion preset", + "no_motion_preset": "No motion preset", + "use_motion_central_config": "Use central motion configuration" + }, + "data_description": { + "motion_sensor_entity_id": "The entity id of the motion sensor", + "motion_delay": "Motion activation delay (seconds)", + "motion_off_delay": "Motion deactivation delay (seconds)", + "motion_preset": "Preset to use when motion is detected", + "no_motion_preset": "Preset to use when no motion is detected", + "use_motion_central_config": "Check to use the central motion configuration. Uncheck to use a specific motion configuration for this VTherm" + } + }, + "power": { + "title": "Power management", + "description": "Power management attributes.\nGives the power and max power sensor of your home.\nSpecify the power consumption of the heater when on.\nAll sensors and device power should use the same unit (kW or W)", + "data": { + "power_sensor_entity_id": "Power", + "max_power_sensor_entity_id": "Max power", + "power_temp": "Shedding temperature", + "use_power_central_config": "Use central power configuration" + }, + "data_description": { + "power_sensor_entity_id": "Power sensor entity id", + "max_power_sensor_entity_id": "Max power sensor entity id", + "power_temp": "Temperature for Power shedding", + "use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm" + } + }, + "presence": { + "title": "Presence management", + "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting", + "data": { + "presence_sensor_entity_id": "Presence sensor", + "use_presence_central_config": "Use central presence temperature configuration. Deselect to use specific temperature entities" + }, + "data_description": { + "presence_sensor_entity_id": "Presence sensor entity id" + } + }, + "advanced": { + "title": "Advanced parameters", + "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation", + "data": { + "safety_delay_min": "Safety delay (in minutes)", + "safety_min_on_percent": "Minimum power percent to enable safety mode", + "safety_default_on_percent": "Power percent to use in safety mode", + "repair_incorrect_state": "Repair incorrect state", + "use_advanced_central_config": "Use central advanced configuration" + }, + "data_description": { + "safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state", + "safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset", + "safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", + "repair_incorrect_state": "Automatically repair incorrect state of underlying entities when detected. The feature will re-send the desired command if the underlying device state doesn't match the expected state.", + "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. 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", + "central_boiler_activation_delay_sec": "Activation delay (seconds)", + "keep_alive_boiler_delay_sec": "Keep-alive delay (seconds)" + }, + "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]", + "central_boiler_activation_delay_sec": "Delay in seconds before activating the central boiler when it needs to be turned on", + "keep_alive_boiler_delay_sec": "Delay in seconds to periodically resend the boiler command if keep-alive is enabled. Set to 0 to disable keep-alive." + } + }, + "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", + "opening_threshold_degree": "Opening threshold degree", + "min_opening_degrees": "Min opening degrees", + "max_opening_degrees": "Max opening degrees", + "max_closing_degree": "Max closing degree" + }, + "data_description": { + "offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV has 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 has the entity for better regulation. There should be one per underlying climate entities", + "proportional_function": "Algorithm to use (other algorithms can be installed as {plugins_link})", + "opening_threshold_degree": "Valve opening value under which the valve should be considered as closed (and then 'max_closing_degree' will apply)", + "min_opening_degrees": "Opening degree minimum value for each underlying device when previous threshold is exceeded, comma separated. Default to 0. Example: 20, 25, 30", + "max_opening_degrees": "Opening degree maximum value. The valve will never open more than this value", + "max_closing_degree": "Closing degree maximum value. The valve will never be closed above this value. Set it to 100 to fully close the valve" + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - Configuration", + "description": "General settings for Auto TPI learning.", + "data": { + "auto_tpi_learning_type": "Learning Type", + "heater_heating_time": "Heating time (min)", + "heater_cooling_time": "Cooling time (min)", + "auto_tpi_heating_rate": "Heating Rate ({unit}/h)", + "auto_tpi_aggressiveness": "Aggressiveness", + "auto_tpi_enable_advanced_settings": "Enable Advanced Parameters", + "auto_tpi_continuous_kext": "Continuous Kext Learning" + }, + "data_description": { + "auto_tpi_learning_type": "Choose 'Discovery' for initial startup (Weighted Average, weight 1) or 'Fine Tuning' for continuous adjustments (EWMA, Alpha 0.08).", + "heater_heating_time": "Time to reach full power (minutes)", + "heater_cooling_time": "Time to cool down after stop (minutes)\n\n| Type | Heating time | Cooling time |\n| :--- | :--- | :--- |\n| Electric radiator | 5mn | 7mn |\n| Water radiator | 15mn | 20mn |\n| Underfloor heating | 30mn | 45mn |", + "auto_tpi_heating_rate": "Radiator temperature rise capacity ({unit} per hour). Leave at 0 for automatic learning (bootstrap).", + "auto_tpi_aggressiveness": "Reduction factor for learned coefficients (50-100%). Lower values result in more conservative coefficients and reduce overshoot risk.", + "auto_tpi_enable_advanced_settings": "Check to access parameter fine-tuning (Alpha, Decay, Weight).", + "auto_tpi_continuous_kext": "Check to enable continuous learning of the external coefficient (Kext) outside of AutoTPI sessions." + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - Weighted Average", + "description": "Parameters for Weighted Average method.", + "data": { + "auto_tpi_avg_initial_weight": "Initial Weight" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Weight of the initial/previous value in the average calculation" + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA", + "description": "Parameters for EWMA method.\n\nRecommendations:\n| Situation | Alpha (ema_alpha) | Decay Rate (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Initial Learning | 0.15 | 0.08 |\n| Fine Learning | 0.08 | 0.12 |\n| Continuous Learning | 0.05 | 0.02 |", + "data": { + "auto_tpi_ema_alpha": "Alpha", + "auto_tpi_ema_decay_rate": "Decay Rate", + "auto_tpi_continuous_kext_alpha": "Continuous Kext Alpha" + }, + "data_description": { + "auto_tpi_ema_alpha": "Smoothing factor (0-1). Higher = faster adaptation", + "auto_tpi_ema_decay_rate": "Rate at which Alpha decreases over time (stabilization)", + "auto_tpi_continuous_kext_alpha": "Smoothing factor for continuous Kext learning (Alpha). Default 0.04 (approx 3-5 days adaptation)." + } + }, + "sync_device_internal_temp": { + "title": "Synchronize device internal temperature", + "description": "Configuration to synchronize the internal temperature of the underlying devices with selected entities", + "data": { + "sync_with_calibration": "Apply offset calibration", + "sync_entity_ids": "Entities id used for synchronization" + }, + "data_description": { + "sync_with_calibration": "Check to apply the offset calibration when synchronizing the internal temperature. Uncheck to directly copy the temperature from the selected entities", + "sync_entity_ids": "The list of the entities used to synchronize the internal temperature of the underlying devices. There should be one per underlying device. Should be a calibration entity if checkbox is selected or a temperature entity otherwise. Both are `number` entities." + } + }, + "heating_failure_detection": { + "title": "Heating failure detection", + "description": "Configure the heating failure detection feature. This detects anomalies when heating is expected but temperature doesn't increase, or when heating is off but temperature keeps rising.", + "data": { + "use_heating_failure_detection_feature": "Enable heating failure detection", + "use_heating_failure_detection_central_config": "Use central heating failure detection configuration", + "heating_failure_threshold": "Heating failure threshold", + "cooling_failure_threshold": "Cooling failure threshold", + "heating_failure_detection_delay": "Detection delay (minutes)", + "temperature_change_tolerance": "Temperature change tolerance (°C)", + "failure_detection_enable_template": "Detection enable template" + }, + "data_description": { + "use_heating_failure_detection_feature": "Enable the detection of heating anomalies for VTherms with TPI", + "use_heating_failure_detection_central_config": "Check to use the central heating failure detection configuration. Uncheck to use specific parameters for this VTherm", + "heating_failure_threshold": "On percent threshold above which heating should cause temperature increase (0.9 = 90%)", + "cooling_failure_threshold": "On percent threshold below which temperature should not increase (0.0 = 0%)", + "heating_failure_detection_delay": "Time in minutes to wait before checking if temperature has changed as expected", + "temperature_change_tolerance": "Minimum temperature change in degrees to consider significant. Smaller changes are ignored to filter sensor noise (default: 0.5°C)", + "failure_detection_enable_template": "An optional Jinja2 template that must return True to enable detection. Useful to temporarily disable detection when an external heat source is active (e.g., `is_state('binary_sensor.wood_stove', 'off')` in double curly braces). If empty, detection is always enabled." + } + } + }, + "error": { + "unknown": "Unexpected error", + "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", + "valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings", + "sync_device_internal_temp_nb_entities_incorrect": "The number of entities for synchronize device internal temperature should be equal to the number of underlyings", + "min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30", + "max_opening_degrees_format": "A comma separated list of positive integer between 0 and the max valve opening is expected. Example: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "For each underlying device, max_opening_degrees must be strictly greater than min_opening_degrees. Please check your configuration.", + "vswitch_configuration_incorrect": "The command customization configuration is incorrect. It is required for underlying entities that are not switches, and the format must be service_name[/attribute:value]. More information is available in the README" + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "options": { + "flow_title": "Versatile Thermostat configuration", + "step": { + "user": { + "title": "Type - {name}", + "description": "Choose thermostat type you want to create", + "data": { + "thermostat_type": "Thermostat type" + }, + "data_description": { + "thermostat_type": "Only one central configuration type is possible" + } + }, + "menu": { + "title": "Menu", + "description": "Configure your thermostat. You will be able to finalize the configuration when all required parameters are entered.", + "menu_options": { + "main": "Main attributes", + "central_boiler": "Central boiler", + "type": "Underlyings", + "tpi": "TPI parameters", + "features": "Features", + "presets": "Presets", + "window": "Window detection", + "motion": "Motion detection", + "power": "Power management", + "presence": "Presence detection", + "advanced": "Advanced parameters", + "auto_start_stop": "Auto start and stop", + "valve_regulation": "Valve regulation configuration", + "sync_device_internal_temp": "Synchronize device temperature", + "lock": "Lock", + "heating_failure_detection": "Heating failure detection", + "finalize": "All done", + "configuration_not_complete": "Configuration not complete" + } + }, + "lock": { + "title": "Lock management - {name}", + "description": "The Lock feature prevents changes to a thermostat's configuration from the UI or automations while keeping the thermostat operational.", + "data": { + "use_lock_central_config": "Use central lock configuration", + "lock_code": "Optional pincode (4 digits)", + "lock_users": "Lock will prevent commands from Users", + "lock_automations": "Lock will prevent commands from Automations & integrations (i.e scheduler)", + "auto_relock_sec": "Auto-relock delay" + }, + "data_description": { + "use_lock_central_config": "Check to use the central lock configuration. Uncheck to use a specific lock configuration for this VTherm", + "auto_relock_sec": "Delay in seconds before automatically re-locking after an unlock. Set to 0 to disable auto-relock. Default is 30 seconds." + } + }, + "main": { + "title": "Main - {name}", + "description": "Main mandatory attributes", + "data": { + "name": "Name", + "thermostat_type": "Thermostat type", + "temperature_sensor_entity_id": "Room temperature", + "last_seen_temperature_sensor_entity_id": "Last seen room temperature datetime", + "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id", + "cycle_min": "Cycle duration (minutes)", + "temp_min": "Minimum temperature allowed", + "temp_max": "Maximum temperature allowed", + "step_temperature": "Temperature step", + "device_power": "Device power", + "use_central_mode": "Enable the control by central entity (requires central config). Check to enable the control of the VTherm with the select central_mode entities.", + "use_main_central_config": "Use additional central main configuration. Check to use the central main configuration (outdoor temperature, min, max, step, ...).", + "used_by_controls_central_boiler": "Used by central boiler. Check if this VTherm should have control on the central boiler" + }, + "data_description": { + "cycle_min": "Minimum duration of one heating/cooling cycle in minutes to avoid too frequent switching", + "temp_min": "Minimum temperature allowed for the thermostat", + "temp_max": "Maximum temperature allowed for the thermostat", + "step_temperature": "Temperature step for setting the target temperature", + "temperature_sensor_entity_id": "Room temperature sensor entity id", + "last_seen_temperature_sensor_entity_id": "Last seen room temperature sensor entity id. Should be datetime sensor", + "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" + } + }, + "features": { + "title": "Features - {name}", + "description": "Thermostat features", + "data": { + "use_window_feature": "Use window detection", + "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 selecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page", + "use_auto_start_stop_feature": "Use the auto start and stop feature", + "use_heating_failure_detection_feature": "Use heating failure detection" + } + }, + "type": { + "title": "Entities - {name}", + "description": "Linked entities attributes", + "data": { + "underlying_entity_ids": "The device(s) to be controlled", + "heater_keep_alive": "Switch keep-alive interval in seconds", + "proportional_function": "Algorithm", + "ac_mode": "AC mode", + "sync_device_internal_temp": "Synchronize device temperature", + "auto_regulation_mode": "Self-regulation", + "auto_regulation_dtemp": "Regulation threshold", + "auto_regulation_periode_min": "Regulation minimum period", + "auto_regulation_use_device_temp": "Use internal temperature of the underlying", + "inverse_switch_command": "Inverse switch command", + "auto_fan_mode": "Auto fan mode", + "on_command_text": "Turn on command customization", + "vswitch_on_command": "Optional turn on commands", + "off_command_text": "Turn off command customization", + "vswitch_off_command": "Optional turn off commands" + }, + "data_description": { + "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 (other algorithms can be installed as {plugins_link})", + "ac_mode": "Use the Air Conditioning (AC) mode", + "sync_device_internal_temp": "Check to synchronize the internal temperature of the underlying device (fill the new menu item 'Synchronize device temperature' to configure it)", + "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", + "auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation (not needed in most cases)", + "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", + "auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary", + "on_command_text": "For underlying of type `select` or `climate` you have to customize the commands." + } + }, + "tpi": { + "title": "TPI - {name}", + "description": "Time Proportional Integral attributes", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "tpi_threshold_low": "Low TPI threshold", + "tpi_threshold_high": "High TPI threshold", + "use_tpi_central_config": "Use central TPI configuration", + "minimal_activation_delay": "Minimum activation delay", + "minimal_deactivation_delay": "Minimum deactivation delay", + "auto_tpi_mode": "Enable Auto TPI Learning" + }, + "data_description": { + "tpi_coef_int": "Coefficient to use for internal temperature delta", + "tpi_coef_ext": "Coefficient to use for external temperature delta", + "tpi_threshold_low": "Threshold in ° under which the TPI algorithm will be on. 0 means no threshold", + "tpi_threshold_high": "Threshold in ° above which the TPI algorithm will be off. 0 means no threshold", + "use_tpi_central_config": "Check to use the central TPI configuration. Uncheck to use a specific TPI configuration for this VTherm", + "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", + "minimal_deactivation_delay": "Delay in seconds under which the equipment will be kept active", + "auto_tpi_mode": "TPI learning session are started manually, read the doc first." + } + }, + "presets": { + "title": "Presets - {name}", + "description": "Check if the thermostat will use central presets. Uncheck and the thermostat will have its own preset entities", + "data": { + "use_presets_central_config": "Use central presets configuration" + } + }, + "window": { + "title": "Window - {name}", + "description": "Open window management.\nYou can also configure automatic window open detection based on temperature decrease", + "data": { + "window_sensor_entity_id": "Window sensor entity id", + "window_delay": "Window sensor 'on' delay (seconds)", + "window_off_delay": "Window sensor 'off' delay (seconds)", + "window_auto_open_threshold": "Temperature decrease threshold for automatic window open detection (in °/hours)", + "window_auto_close_threshold": "Temperature increase threshold for end of automatic detection (in °/hours)", + "window_auto_max_duration": "Maximum duration of automatic window open detection (in min)", + "use_window_central_config": "Use central window configuration", + "window_action": "Action" + }, + "data_description": { + "window_sensor_entity_id": "Leave empty if no window sensor should be used and to use the automatic detection", + "window_delay": "The delay in seconds before sensor 'on' detection is taken into account", + "window_off_delay": "The delay in seconds before sensor 'off' detection is taken into account. Leave it empty to use the same value as window on delay", + "window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used", + "window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used", + "window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used", + "use_window_central_config": "Check to use the central window configuration. Uncheck to use a specific window configuration for this VTherm", + "window_action": "Action to do if window is deteted as open" + } + }, + "motion": { + "title": "Motion - {name}", + "description": "Motion management. Preset can switch automatically depending of a motion detection\nmotion_preset and no_motion_preset should be set to the corresponding preset name", + "data": { + "motion_sensor_entity_id": "Motion sensor entity id", + "motion_delay": "Activation delay", + "motion_off_delay": "Deactivation delay", + "motion_preset": "Motion preset", + "no_motion_preset": "No motion preset", + "use_motion_central_config": "Use central motion configuration" + }, + "data_description": { + "motion_sensor_entity_id": "The entity id of the motion sensor", + "motion_delay": "Motion activation delay (seconds)", + "motion_off_delay": "Motion deactivation delay (seconds)", + "motion_preset": "Preset to use when motion is detected", + "no_motion_preset": "Preset to use when no motion is detected", + "use_motion_central_config": "Check to use the central motion configuration. Uncheck to use a specific motion configuration for this VTherm" + } + }, + "power": { + "title": "Power - {name}", + "description": "Power management attributes.\nGives the power and max power sensor of your home.\nSpecify the power consumption of the heater when on.\nAll sensors and device power should use the same unit (kW or W)", + "data": { + "power_sensor_entity_id": "Power", + "max_power_sensor_entity_id": "Max power", + "power_temp": "Shedding temperature", + "use_power_central_config": "Use central power configuration" + }, + "data_description": { + "power_sensor_entity_id": "Power sensor entity id", + "max_power_sensor_entity_id": "Max power sensor entity id", + "power_temp": "Temperature for Power shedding", + "use_power_central_config": "Check to use the central power configuration. Uncheck to use a specific power configuration for this VTherm" + } + }, + "presence": { + "title": "Presence - {name}", + "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present) and give the corresponding temperature preset setting", + "data": { + "presence_sensor_entity_id": "Presence sensor", + "use_presence_central_config": "Use central presence temperature configuration. Uncheck to use specific temperature entities" + }, + "data_description": { + "presence_sensor_entity_id": "Presence sensor entity id" + } + }, + "advanced": { + "title": "Advanced - {name}", + "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation", + "data": { + "minimal_activation_delay": "Minimum activation delay", + "minimal_deactivation_delay": "Minimum deactivation delay", + "safety_delay_min": "Safety delay (in minutes)", + "safety_min_on_percent": "Minimum power percent to enable safety mode", + "safety_default_on_percent": "Power percent to use in safety mode", + "repair_incorrect_state": "Repair incorrect state", + "use_advanced_central_config": "Use central advanced configuration" + }, + "data_description": { + "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", + "minimal_deactivation_delay": "Delay in seconds under which the equipment will be kept active", + "safety_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a safety off state", + "safety_min_on_percent": "Minimum heating percent value for safety preset activation. Below this amount of power percent the thermostat won't go into safety preset", + "safety_default_on_percent": "The default heating power percent value in safety preset. Set to 0 to switch off heater in safety preset", + "repair_incorrect_state": "Automatically repair incorrect state of underlying entities when detected. The feature will re-send the desired command if the underlying device state doesn't match the expected state.", + "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. 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", + "central_boiler_activation_delay_sec": "Activation delay (seconds)", + "keep_alive_boiler_delay_sec": "Keep-alive delay (seconds)" + }, + "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]", + "central_boiler_activation_delay_sec": "Delay in seconds before activating the central boiler when it needs to be turned on", + "keep_alive_boiler_delay_sec": "Delay in seconds to periodically resend the boiler command if keep-alive is enabled. Set to 0 to disable keep-alive." + } + }, + "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", + "opening_threshold_degree": "Opening threshold degree", + "min_opening_degrees": "Min opening degrees", + "max_opening_degrees": "Max opening degrees", + "max_closing_degree": "Max closing degree" + }, + "data_description": { + "offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV has 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 has the entity for better regulation. There should be one per underlying climate entities", + "proportional_function": "Algorithm to use (other algorithms can be installed as {plugins_link})", + "opening_threshold_degree": "Valve opening value under which the valve should be considered as closed (and then 'max_closing_degree' will apply)", + "min_opening_degrees": "Opening degree minimum value for each underlying device when previous threshold is exceeded, comma separated. Default to 0. Example: 20, 25, 30", + "max_opening_degrees": "Opening degree maximum value. The valve will never open more than this value", + "max_closing_degree": "Closing degree maximum value. The valve will never be closed above this value. Set it to 100 to fully close the valve" + } + }, + "sync_device_internal_temp": { + "title": "Synchronize device internal temperature", + "description": "Configuration to synchronize the internal temperature of the underlying devices with selected entities", + "data": { + "sync_with_calibration": "Apply offset calibration", + "sync_entity_ids": "Entities id used for synchronization" + }, + "data_description": { + "sync_with_calibration": "Check to apply the offset calibration when synchronizing the internal temperature. Uncheck to directly copy the temperature from the selected entities", + "sync_entity_ids": "The list of the entities used to synchronize the internal temperature of the underlying devices. There should be one per underlying device. Should be a calibration entity if checkbox is selected or a temperature entity otherwise. Both are `number` entities." + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - Configuration - {name}", + "description": "General settings for Auto TPI learning.", + "data": { + "auto_tpi_learning_type": "Learning Type", + "auto_tpi_heating_rate": "Heating Rate ({unit}/h)", + "heater_heating_time": "Heating time (min)", + "heater_cooling_time": "Cooling time (min)", + "auto_tpi_aggressiveness": "Aggressiveness", + "auto_tpi_enable_advanced_settings": "Enable Advanced Parameters", + "auto_tpi_continuous_kext": "Continuous Kext Learning" + }, + "data_description": { + "auto_tpi_learning_type": "Choose 'Discovery' for initial startup (Weighted Average, weight 1) or 'Fine Tuning' for continuous adjustments (EWMA, Alpha 0.08).", + "auto_tpi_heating_rate": "Radiator temperature rise capacity ({unit} per hour). Leave at 0 for automatic learning (bootstrap).", + "heater_heating_time": "Time to reach full power (minutes)", + "heater_cooling_time": "Time to cool down after stop (minutes)\n\n| Type | Heating time | Cooling time |\n| :--- | :--- | :--- |\n| Electric radiator | 5mn | 7mn |\n| Water radiator | 15mn | 20mn |\n| Underfloor heating | 30mn | 45mn |", + "auto_tpi_aggressiveness": "Reduction factor for learned coefficients (50-100%). Lower values result in more conservative coefficients and reduce overshoot risk.", + "auto_tpi_enable_advanced_settings": "Check to modify the parameters of the chosen algorithm.", + "auto_tpi_continuous_kext": "Check to enable continuous learning of the external coefficient (Kext) outside of AutoTPI sessions." + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - Weighted Average - {name}", + "description": "Parameters for Weighted Average method.", + "data": { + "auto_tpi_avg_initial_weight": "Initial Weight" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Initial weight of existing coefficients in the average calculation (1-50 is typical). Higher weight means slower and more stable learning." + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA - {name}", + "description": "Parameters for EWMA method.\n\nRecommendations:\n| Situation | Alpha (ema_alpha) | Decay Rate (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Strong adjustments | 0.15 | 0.08 |\n| Fine tuning | 0.08 | 0.12 |", + "data": { + "auto_tpi_ema_alpha": "Alpha", + "auto_tpi_ema_decay_rate": "Decay Rate", + "auto_tpi_continuous_kext_alpha": "Continuous Kext Alpha" + }, + "data_description": { + "auto_tpi_ema_alpha": "Smoothing factor (0-1). Higher = faster adaptation", + "auto_tpi_ema_decay_rate": "Rate at which Alpha decreases over time (stabilization)", + "auto_tpi_continuous_kext_alpha": "Smoothing factor for continuous Kext learning (Alpha). Default 0.04 (approx 3-5 days adaptation)." + } + }, + "heating_failure_detection": { + "title": "Heating failure detection - {name}", + "description": "Configure the heating failure detection feature. This detects anomalies when heating is expected but temperature doesn't increase, or when heating is off but temperature keeps rising.", + "data": { + "use_heating_failure_detection_feature": "Enable heating failure detection", + "use_heating_failure_detection_central_config": "Use central heating failure detection configuration", + "heating_failure_threshold": "Heating failure threshold", + "cooling_failure_threshold": "Cooling failure threshold", + "heating_failure_detection_delay": "Detection delay (minutes)", + "temperature_change_tolerance": "Temperature change tolerance (°C)", + "failure_detection_enable_template": "Detection enable template" + }, + "data_description": { + "use_heating_failure_detection_feature": "Enable the detection of heating anomalies for VTherms with TPI", + "use_heating_failure_detection_central_config": "Check to use the central heating failure detection configuration. Uncheck to use specific parameters for this VTherm", + "heating_failure_threshold": "On percent threshold above which heating should cause temperature increase (0.9 = 90%)", + "cooling_failure_threshold": "On percent threshold below which temperature should not increase (0.0 = 0%)", + "heating_failure_detection_delay": "Time in minutes to wait before checking if temperature has changed as expected", + "temperature_change_tolerance": "Minimum temperature change in degrees to consider significant. Smaller changes are ignored to filter sensor noise (default: 0.5°C)", + "failure_detection_enable_template": "An optional Jinja2 template that must return True to enable detection. Useful to temporarily disable detection when an external heat source is active (e.g., `is_state('binary_sensor.wood_stove', 'off')` in double curly braces). If empty, detection is always enabled." + } + } + }, + "error": { + "unknown": "Unexpected error", + "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", + "valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings", + "sync_device_internal_temp_nb_entities_incorrect": "The number of entities for synchronize device internal temperature should be equal to the number of underlyings", + "min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30", + "max_opening_degrees_format": "A comma separated list of positive integer between 0 and the max value of the valve is expected. Example: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "For each underlying device, max_opening_degrees must be strictly greater than min_opening_degrees. Please check your configuration.", + "vswitch_configuration_incorrect": "The command customization configuration is incorrect. It is required for underlying entities that are not switches, and the format must be service_name[/attribute:value]. More information is available in the README" + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "selector": { + "thermostat_type": { + "options": { + "thermostat_central_config": "Central configuration", + "thermostat_over_switch": "Thermostat over a switch", + "thermostat_over_climate": "Thermostat over a climate", + "thermostat_over_valve": "Thermostat over a valve" + } + }, + "auto_regulation_mode": { + "options": { + "auto_regulation_slow": "Slow", + "auto_regulation_strong": "Strong", + "auto_regulation_medium": "Medium", + "auto_regulation_light": "Light", + "auto_regulation_expert": "Expert", + "auto_regulation_none": "No auto-regulation", + "auto_regulation_valve": "Direct control of valve" + } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "No auto fan", + "auto_fan_low": "Low", + "auto_fan_medium": "Medium", + "auto_fan_high": "High", + "auto_fan_turbo": "Turbo" + } + }, + "window_action": { + "options": { + "window_turn_off": "Turn off", + "window_fan_only": "Fan only", + "window_frost_temp": "Frost protect", + "window_eco_temp": "Eco" + } + }, + "presets": { + "options": { + "frost": "Frost protect", + "eco": "Eco", + "comfort": "Comfort", + "boost": "Boost" + } + }, + "auto_start_stop": { + "options": { + "auto_start_stop_none": "No auto start/stop", + "auto_start_stop_very_slow": "Very slow detection", + "auto_start_stop_slow": "Slow detection", + "auto_start_stop_medium": "Medium detection", + "auto_start_stop_fast": "Fast detection" + } + }, + "auto_tpi_calculation_method": { + "options": { + "average": "Average", + "ema": "Exponential Moving Average (EMA)" + } + }, + "auto_tpi_learning_type": { + "options": { + "discovery": "Discovery", + "fine_tuning": "Fine Tuning" + } + } + }, + "entity": { + "climate": { + "versatile_thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "power": "Shedding", + "safety": "Safety", + "none": "Manual", + "frost": "Frost" + } + } + } + } + }, + "binary_sensor": { + "safety_state": { + "name": "Safety state" + }, + "heating_failure_state": { + "name": "Heating failure state" + }, + "overpowering_state": { + "name": "Overpowering state" + }, + "window_state": { + "name": "Window state" + }, + "motion_state": { + "name": "Motion state" + }, + "presence_state": { + "name": "Presence state" + }, + "window_bypass_state": { + "name": "Window bypass" + }, + "central_boiler_state": { + "name": "Central boiler" + } + }, + "number": { + "frost_temp": { + "name": "Frost" + }, + "eco_temp": { + "name": "Eco" + }, + "comfort_temp": { + "name": "Comfort" + }, + "boost_temp": { + "name": "Boost" + }, + "frost_ac_temp": { + "name": "Frost ac" + }, + "eco_ac_temp": { + "name": "Eco ac" + }, + "comfort_ac_temp": { + "name": "Comfort ac" + }, + "boost_ac_temp": { + "name": "Boost ac" + }, + "frost_away_temp": { + "name": "Frost away" + }, + "eco_away_temp": { + "name": "Eco away" + }, + "comfort_away_temp": { + "name": "Comfort away" + }, + "boost_away_temp": { + "name": "Boost away" + }, + "eco_ac_away_temp": { + "name": "Eco ac away" + }, + "comfort_ac_away_temp": { + "name": "Comfort ac away" + }, + "boost_ac_away_temp": { + "name": "Boost ac away" + } + } + }, + "services": { + "set_auto_tpi_mode": { + "name": "Set Auto TPI mode", + "description": "Enable or disable the Auto TPI learning mode", + "fields": { + "auto_tpi_mode": { + "name": "Auto TPI mode", + "description": "Enable (true) or disable (false) the Auto TPI learning" + }, + "reinitialise": { + "name": "Reset learning data", + "description": "Reset all learning data when enabling Auto TPI mode (default is true)" + } + } + }, + "reset_auto_tpi_capacities": { + "name": "Reset Auto TPI Capacities", + "description": "Reset the learned maximum heating and cooling capacities to 1.0 °C/h. Learned coefficients are NOT reset." + }, + "auto_tpi_calibrate_capacity": { + "name": "Auto Tpi Calibrate Capacity", + "description": "Calibrate the heating/cooling capacity (in °C/hour) using linear regression on the entity's history data. Learned coefficients are NOT updated.", + "fields": { + "start_date": { + "name": "History from date", + "description": "Date from which to retrieve history (default is 30 days ago)" + }, + "end_date": { + "name": "History to date", + "description": "Date until which to retrieve history (default is now)" + }, + "hvac_mode": { + "name": "HVAC Mode", + "description": "Which mode to calibrate (heat or cool)." + }, + "save_to_config": { + "name": "Save to Config", + "description": "Apply the calculated capacity to the Auto TPI configuration." + }, + "min_power_threshold": { + "name": "Minimum Power Threshold", + "description": "Minimum power percentage to consider a cycle as saturated (default 95%). Lower values will include more cycles but may be less accurate." + } + } + }, + "set_tpi_parameters": { + "name": "Set TPI parameters", + "description": "Change the TPI parameters", + "fields": { + "minimal_activation_delay": { + "name": "Minimal Activation delay", + "description": "Delay in seconds under which the equipment will not be activated" + }, + "minimal_deactivation_delay": { + "name": "Minimal Deactivation delay", + "description": "Delay in seconds under which the equipment will be kept active" + }, + "tpi_threshold_low": { + "name": "Low TPI threshold", + "description": "Threshold in ° under which the TPI algorithm will be on. 0 means no threshold" + }, + "tpi_threshold_high": { + "name": "High TPI threshold", + "description": "Threshold in ° above which the TPI algorithm will be off. 0 means no threshold" + } + } + }, + "lock": { + "name": "Lock", + "description": "Locks the thermostat, preventing any changes to its configuration from UI or automations.", + "fields": { + "code": { + "name": "Lock code", + "description": "The optional lock code" + } + } + }, + "unlock": { + "name": "Unlock", + "description": "Unlocks the thermostat, allowing changes to its configuration.", + "fields": { + "code": { + "name": "Lock code", + "description": "The optional lock code" + } + } + }, + "download_logs": { + "name": "Download logs", + "description": "Collect and download filtered logs for a VTherm entity.", + "fields": { + "log_level": { + "name": "Log level", + "description": "Minimum log level to include" + }, + "period_start": { + "name": "Period start", + "description": "Start of the extraction period" + }, + "period_end": { + "name": "Period end", + "description": "End of the extraction period" + } + } + } + }, + "exceptions": { + "auto_tpi_learning_stopped": { + "message": "Auto TPI learning for {name} has been stopped due to 3 consecutive failures. Reason: {reason}. Please check your configuration." + } + } +} \ No newline at end of file diff --git a/custom_components/versatile_thermostat/translations/fr.json b/custom_components/versatile_thermostat/translations/fr.json new file mode 100644 index 0000000..7658a3e --- /dev/null +++ b/custom_components/versatile_thermostat/translations/fr.json @@ -0,0 +1,1051 @@ +{ + "title": "Versatile Thermostat configuration", + "config": { + "flow_title": "Versatile Thermostat configuration", + "step": { + "user": { + "title": "Type du nouveau Versatile Thermostat", + "description": "Choisissez le type de thermostat que vous voulez créer", + "data": { + "thermostat_type": "Type de thermostat" + }, + "data_description": { + "thermostat_type": "Un seul thermostat de type Configuration centrale est possible." + } + }, + "menu": { + "title": "Menu", + "description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.", + "menu_options": { + "main": "Principaux Attributs", + "central_boiler": "Chauffage central", + "type": "Sous-jacents", + "tpi": "Paramètres TPI", + "features": "Fonctions", + "presets": "Pre-réglages", + "window": "Détection d'ouverture", + "motion": "Détection de mouvement", + "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", + "sync_device_internal_temp": "Synchronisation de la température de l'appareil", + "lock": "Gestion du verrouilage", + "heating_failure_detection": "Détection de panne de chauffage", + "auto_tpi_configuration": "Auto TPI - Configuration", + "auto_tpi_avg_settings": "Auto TPI - Moyenne pondérée", + "auto_tpi_ema_settings": "Auto TPI - EWMA", + "finalize": "Finaliser la création", + "configuration_not_complete": "Configuration incomplète" + } + }, + "lock": { + "title": "Gestion du verrouillage", + "description": "La fonction de verrouillage empêche les modifications d'un thermostat depuis l'interface utilisateur ou les automatisations tout en gardant le thermostat opérationnel.", + "data": { + "use_lock_central_config": "Utiliser la configuration de verrouillage centrale", + "lock_code": "Code PIN optionnel (4 chiffres)", + "lock_users": "Le verrouillage empêchera les commandes des utilisateurs", + "lock_automations": "Le verrouillage empêchera les commandes des automatisations et des intégrations (ex: planificateur)", + "auto_relock_sec": "Délai de re-verrouillage automatique" + }, + "data_description": { + "use_lock_central_config": "Cochez pour utiliser la configuration de verrouillage centrale. Décochez pour utiliser une configuration de verrouillage spécifique pour ce VTherm", + "lock_code": "Le code pin pour déverouiller le thermostat (4 digits)", + "auto_relock_sec": "Délai en secondes avant le re-verrouillage automatique après un déverrouillage. Mettre 0 pour désactiver. La valeur par défaut est 30 secondes." + } + }, + "main": { + "title": "Ajout d'un nouveau thermostat", + "description": "Principaux attributs obligatoires", + "data": { + "name": "Nom", + "thermostat_type": "Type de thermostat", + "temperature_sensor_entity_id": "Capteur de température", + "last_seen_temperature_sensor_entity_id": "Dernière vue capteur de température", + "external_temperature_sensor_entity_id": "Capteur de température exterieure", + "cycle_min": "Durée du cycle (minutes)", + "temp_min": "Température minimale permise", + "temp_max": "Température maximale permise", + "step_temperature": "Pas de température", + "device_power": "Puissance de l'équipement", + "use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.", + "use_main_central_config": "Utiliser la configuration centrale supplémentaire. Cochez pour utiliser la configuration centrale supplémentaire (température externe, min, max, pas, ...)", + "used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale." + }, + "data_description": { + "cycle_min": "Durée minimale d'un cycle de chauffage/refroidissement en minutes pour éviter des commutations trop fréquentes", + "temp_min": "Température minimale autorisée pour le thermostat", + "temp_max": "Température maximale autorisée pour le thermostat", + "step_temperature": "Pas de température pour le réglage de la température cible", + "temperature_sensor_entity_id": "Id d'entité du capteur de température", + "last_seen_temperature_sensor_entity_id": "Id d'entité du capteur donnant la date et heure de dernière vue capteur de température. L'état doit être au format date heure (ex: 2024-03-31T17:07:03+00:00)", + "external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. Non utilisé si une configuration centrale est définie" + } + }, + "features": { + "title": "Fonctions", + "description": "Fonctions du thermostat à utiliser", + "data": { + "use_window_feature": "Avec détection des ouvertures", + "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 contrôle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commandent 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", + "use_heating_failure_detection_feature": "Avec détection de panne de chauffage" + } + }, + "type": { + "title": "Entité(s) liée(s)", + "description": "Attributs de(s) l'entité(s) liée(s)", + "data": { + "underlying_entity_ids": "Les équipements à controller", + "heater_keep_alive": "keep-alive (sec)", + "proportional_function": "Algorithme", + "ac_mode": "AC mode ?", + "sync_device_internal_temp": "Synchronisation de la température interne de l'appareil", + "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": "Compenser la température interne du sous-jacent", + "inverse_switch_command": "Inverser la commande", + "auto_fan_mode": " Auto ventilation mode", + "on_command_text": "Personnalisation des commandes d'allumage", + "vswitch_on_command": "Commande d'allumage (optionnel)", + "off_command_text": "Personnalisation des commandes d'extinction", + "vswitch_off_command": "Commande d'extinction (optionnel)" + }, + "data_description": { + "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 (d'autres algorithmes peuvent être installés sous forme de {plugins_link})", + "ac_mode": "Utilisation du mode Air Conditionné (AC)", + "sync_device_internal_temp": "Synchroniser la température interne de l'appareil sous-jacent avec la température mesurée par le VTherm", + "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": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation (n'est pas nécessaire dans la plupart des cas)", + "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", + "on_command_text": "Pour les sous-jacents de type `select` ou `climate` vous devez personnaliser les commandes." + } + }, + "tpi": { + "title": "TPI", + "description": "Attributs de l'algo Time Proportional Integral", + "data": { + "tpi_coef_int": "coeff_int", + "tpi_coef_ext": "coeff_ext", + "tpi_threshold_low": "TPI seuil bas", + "tpi_threshold_high": "TPI seuil haut", + "minimal_activation_delay": "Délai minimal d'activation", + "minimal_deactivation_delay": "Délai de désactivation minimal", + "use_tpi_central_config": "Utiliser la configuration TPI centrale", + "auto_tpi_mode": "Activer l'apprentissage Auto TPI" + }, + "data_description": { + "tpi_coef_int": "Coefficient à utiliser pour le delta de température interne", + "tpi_coef_ext": "Coefficient à utiliser pour le delta de température externe", + "tpi_threshold_low": "Seuil en ° en dessous duquel le TPI sera activé. 0 signifie pas de seuil", + "tpi_threshold_high": "Seuil en ° au-dessus duquel l'algorithme TPI sera désactivé. 0 signifie pas de seuil", + "use_tpi_central_config": "Cochez pour utiliser la configuration TPI centrale. Décochez et saisissez les attributs pour utiliser une configuration TPI spécifique", + "minimal_activation_delay": "Délai en secondes en-dessous duquel l'équipement ne sera pas activé", + "minimal_deactivation_delay": "Délai en secondes en-dessous duquel l'équipement se laissé actif", + "auto_tpi_mode": "Les sessions d’apprentissage TPI sont démarrées manuellement ; veuillez lire la doc au préalable." + } + }, + "presets": { + "title": "Pre-réglages", + "description": "Cochez pour que ce thermostat utilise les pré-réglages de la configuration centrale. Décochez pour utiliser des entités de température spécifiques", + "data": { + "use_presets_central_config": "Utiliser la configuration des pré-réglages centrale" + } + }, + "window": { + "title": "Gestion d'une ouverture", + "description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'id d'entité vide pour utiliser la détection automatique", + "data": { + "window_sensor_entity_id": "Détecteur d'ouverture (entity id)", + "window_delay": "Délai de prise en compte à l'ouverture (secondes)", + "window_off_delay": "Délai de prise compte à la fermeture (secondes)", + "window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)", + "window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)", + "window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)", + "use_window_central_config": "Utiliser la configuration centrale des ouvertures", + "window_action": "Action" + }, + "data_description": { + "window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur et pour utiliser la détection automatique", + "window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte lors de la détection d'une ouverture", + "window_off_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte lors de la détection d'une fermeture. Laissez vide pour utiliser le même délai à l'ouveture et à la fermeture", + "window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. 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", + "use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures", + "window_action": "Action a effectuer si la fenêtre est détectée comme ouverte" + } + }, + "motion": { + "title": "Gestion de la détection de mouvement", + "description": "Le preset s'ajuste automatiquement si un mouvement est détecté\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser", + "data": { + "motion_sensor_entity_id": "Détecteur de mouvement", + "motion_delay": "Délai d'activation", + "motion_off_delay": "Délai de désactivation", + "motion_preset": "Preset si mouvement", + "no_motion_preset": "Preset sans mouvement", + "use_motion_central_config": "Utiliser la condfiguration centrale du mouvement" + }, + "data_description": { + "motion_sensor_entity_id": "Id d'entité du détecteur de mouvement", + "motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondes)", + "motion_off_delay": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)", + "motion_preset": "Preset à utiliser si mouvement détecté", + "no_motion_preset": "Preset à utiliser si pas de mouvement détecté", + "use_motion_central_config": "Cochez pour utiliser la configuration centrale du mouvement. Décochez et saisissez les attributs pour utiliser une configuration spécifique du mouvement" + } + }, + "power": { + "title": "Gestion de la puissance", + "description": "Sélectionne automatiquement le preset 'power' si la puissance consommée est supérieure à un maximum.\nDonnez les entity id des capteurs qui mesurent la puissance totale et la puissance max autorisée.\nEnsuite donnez la puissance de l'équipement.\nTous les capteurs et la puissance consommée par l'équipement doivent avoir la même unité de mesure (kW ou W)", + "data": { + "power_sensor_entity_id": "Capteur de puissance totale (entity id)", + "max_power_sensor_entity_id": "Capteur de puissance Max (entity id)", + "power_temp": "Température si délestaqe", + "use_power_central_config": "Utiliser la configuration centrale de la puissance" + }, + "data_description": { + "power_sensor_entity_id": "Entity id du capteur de puissance totale du logement", + "max_power_sensor_entity_id": "Entity id du capteur de puissance Max autorisée avant délestage", + "power_temp": "Température cible si délestaqe", + "use_power_central_config": "Cochez pour utiliser la configuration centrale de la puissance. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la puissance" + } + }, + "presence": { + "title": "Gestion de la présenc", + "description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence", + "data": { + "presence_sensor_entity_id": "Capteur de présence", + "use_presence_central_config": "Utiliser la configuration centrale des températures en cas d'absence. Décochez pour avoir des entités de température dédiées" + }, + "data_description": { + "presence_sensor_entity_id": "Id d'entité du capteur de présence" + } + }, + "advanced": { + "title": "Paramètres avancés", + "description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat", + "data": { + "safety_delay_min": "Délai maximal entre 2 mesures de températures", + "safety_min_on_percent": "Pourcentage minimal de puissance", + "safety_default_on_percent": "Pourcentage de puissance a utiliser en mode securité", + "repair_incorrect_state": "Réparer l'état incorrect", + "use_advanced_central_config": "Utiliser la configuration centrale avancée" + }, + "data_description": { + "safety_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité", + "safety_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé", + "safety_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité", + "repair_incorrect_state": "Réparer automatiquement l'état incorrect des entités sous-jacentes lorsqu'elles sont détectées. La fonction renverra la commande souhaitée si l'état du dispositif sous-jacent ne correspond pas à l'état attendu.", + "use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée" + } + }, + "central_boiler": { + "title": "Contrôle de la chaudière centrale", + "description": "Donnez les services à appeler pour allumer/éteindre la chaudière centrale. Le service à appeler doit être formatté comme suit: `entity_id/service_name[/attribut:valeur]` (/attribut:valeur est facultatif)\nPar exemple:\n- pour allumer un switch: `switch.controle_chaudiere/switch.turn_on`\n- pour éteindre un switch: `switch.controle_chaudiere/switch.turn_off`\n- pour programmer la chaudière sur 25° et ainsi forcer son allumage: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- pour envoyer 10° à la chaudière et ainsi forcer son extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`", + "data": { + "central_boiler_activation_service": "Commande pour allumer", + "central_boiler_deactivation_service": "Commande pour éteindre", + "central_boiler_activation_delay_sec": "Délai d'activation (en secondes)", + "keep_alive_boiler_delay_sec": "Délai du maintien actif (en secondes)" + }, + "data_description": { + "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 éteindre la chaudière centrale au format entity_id/service_name[/attribut:valeur]", + "central_boiler_activation_delay_sec": "Délai en secondes avant d'activer la chaudière centrale après qu'un VTherm ait demandé l'allumage", + "keep_alive_boiler_delay_sec": "Délai en secondes pour relancer périodiquement la commande de la chaudière si le maintien actif est activé. Mettre 0 pour désactiver." + } + }, + "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", + "opening_threshold_degree": "Seuil d'ouverture", + "min_opening_degrees": "Ouvertures minimales", + "max_opening_degrees": "Ouvertures maximales", + "max_closing_degree": "Fermetures maximale" + }, + "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 (d'autres algorithmes peuvent être installés sous forme de {plugins_link})", + "opening_threshold_degree": "Seuil d'ouverture la vanne en dessous de laquelle la vanne est considérée comme devant être fermée (et alors 'max_closing_degree' s'appliquera)", + "min_opening_degrees": "Ouverture minimale pour chaque vanne considérée comme chauffante, séparée par des virgules. Par défaut à 0. Exemple : 20, 25, 30", + "max_opening_degrees": "Degré d'ouverture maximum pour chaque équipement sous-jacent, séparé par des virgules. Par défaut 100. La vanne ne s'ouvrira jamais plus que cette valeur. Exemple: 80, 85, 90", + "max_closing_degree": "Fermeture maximale. La vanne ne sera jamais fermée au-delà de cette valeur. Mettez à 100 pour fermer complètement la vanne" + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - Configuration", + "description": "Paramètres généraux de l'apprentissage Auto TPI.", + "data": { + "auto_tpi_learning_type": "Type d'apprentissage", + "heater_heating_time": "Temps de chauffe (min)", + "heater_cooling_time": "Temps de refroidissement (min)", + "auto_tpi_heating_rate": "Taux de chauffe ({unit}/h)", + "auto_tpi_aggressiveness": "Agressivité", + "auto_tpi_enable_advanced_settings": "Activer les paramètres avancés", + "auto_tpi_continuous_kext": "Apprentissage continu de Kext" + }, + "data_description": { + "auto_tpi_learning_type": "Choisissez 'Découverte' pour le démarrage initial (Moyenne pondérée, poids 1) ou 'Réglage Fin' pour des ajustements continus (EWMA, Alpha 0.08).", + "heater_heating_time": "Temps pour atteindre la pleine puissance (minutes)", + "heater_cooling_time": "Temps pour refroidir après arrêt (minutes)\n\n| Type | Temps de chauffe | Temps de refroidissement |\n| :--- | :--- | :--- |\n| Radiateur électrique | 5mn | 7mn |\n| Radiateur à eau | 15mn | 20mn |\n| Chauffage par le sol | 30mn | 45mn |", + "auto_tpi_heating_rate": "Capacité de montée en température du radiateur ({unit} par heure). Laisser à 0 pour l'apprentissage automatique (bootstrap).", + "auto_tpi_aggressiveness": "Facteur de réduction des coefficients appris (50-100%). Des valeurs plus basses donnent des coefficients plus conservateurs et réduisent le risque de dépassement de consigne.", + "auto_tpi_enable_advanced_settings": "Cochez pour accéder au réglage fin des paramètres (Alpha, Décroissance, Poids).", + "auto_tpi_continuous_kext": "Cochez pour activer l'ajustement continu du coefficient extérieur (Kext) hors session AutoTPI." + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - Moyenne pondérée", + "description": "Paramètres pour la méthode Moyenne pondérée.", + "data": { + "auto_tpi_avg_initial_weight": "Poids Initial" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Poids de la valeur initiale/précédente dans le calcul de la moyenne" + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA", + "description": "Paramètres pour la méthode EWMA.\n\nRecommandations :\n| Situation | Alpha (ema_alpha) | Taux de Décroissance (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Apprentissage initial | 0.15 | 0.08 |\n| Apprentissage fin | 0.08 | 0.12 |\n| Apprentissage continu | 0.05 | 0.02 |", + "data": { + "auto_tpi_ema_alpha": "Alpha", + "auto_tpi_ema_decay_rate": "Taux de décroissance", + "auto_tpi_continuous_kext_alpha": "Alpha Apprentissage Continu" + }, + "data_description": { + "auto_tpi_ema_alpha": "Facteur de lissage (0-1). Plus élevé = adaptation plus rapide", + "auto_tpi_ema_decay_rate": "Taux auquel Alpha diminue avec le temps (stabilisation)", + "auto_tpi_continuous_kext_alpha": "Facteur de lissage pour l'apprentissage continu (Alpha). Défaut 0.04 (adaptation env. 3-5 jours)." + } + }, + "sync_device_internal_temp": { + "title": "Synchronisation de la température interne des sous-jacents", + "description": "Configuration de la synchronisation des températures internes des sous-jacents", + "data": { + "sync_with_calibration": "Application d'un calibrage", + "sync_entity_ids": "Identifiants des entités utilisées pour la synchronisation" + }, + "data_description": { + "sync_with_calibration": "Cochez pour appliquer le calibrage du décalage lors de la synchronisation de la température interne. Décochez pour copier directement la température vers les entités sélectionnées", + "sync_entity_ids": "La liste des entités utilisées pour synchroniser la température interne des appareils sous-jacents. Il doit y en avoir une par appareil sous-jacent. Doit être une entité de calibrage si la case est cochée ou une entité de température sinon. Les deux sont des entités de type `number`." + } + }, + "heating_failure_detection": { + "title": "Détection de panne de chauffage", + "description": "Configuration de la détection de panne de chauffage. Cette fonctionnalité détecte les anomalies lorsque le chauffage est demandé mais que la température n'augmente pas, ou lorsque le chauffage est éteint mais que la température continue de monter.", + "data": { + "use_heating_failure_detection_feature": "Activer la détection de panne de chauffage", + "use_heating_failure_detection_central_config": "Utiliser la configuration centrale de détection de panne", + "heating_failure_threshold": "Seuil de panne de chauffage", + "cooling_failure_threshold": "Seuil de panne de refroidissement", + "heating_failure_detection_delay": "Délai de détection (minutes)", + "temperature_change_tolerance": "Tolérance (°C)", + "failure_detection_enable_template": "Template d'activation de la détection" + }, + "data_description": { + "use_heating_failure_detection_feature": "Activer la détection d'anomalies de chauffage pour les VTherms avec TPI", + "use_heating_failure_detection_central_config": "Cochez pour utiliser la configuration centrale de détection de panne. Décochez pour utiliser des paramètres spécifiques à ce VTherm", + "heating_failure_threshold": "Seuil de on_percent au-dessus duquel le chauffage devrait faire monter la température (0.9 = 90%)", + "cooling_failure_threshold": "Seuil de on_percent en-dessous duquel la température ne devrait pas augmenter (0.0 = 0%)", + "heating_failure_detection_delay": "Temps en minutes à attendre avant de vérifier si la température a évolué comme attendu", + "temperature_change_tolerance": "Variation minimale de température en degrés pour être considérée comme significative. Les variations plus petites sont ignorées pour filtrer le bruit des capteurs (défaut: 0.5°C)", + "failure_detection_enable_template": "Un template Jinja2 optionnel qui doit retourner True pour activer la détection. Utile pour désactiver temporairement la détection lorsqu'une source de chaleur externe est active (ex: `is_state('binary_sensor.poele_a_bois', 'off')` entre double accolades). Si vide, la détection est toujours active." + } + } + }, + "error": { + "unknown": "Erreur inattendue", + "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", + "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", + "sync_device_internal_temp_nb_entities_incorrect": "Le nombre d'entités pour la synchronisation de la température interne des appareils sous-jacents doit être égal au nombre d'appareils sous-jacents", + "min_opening_degrees_format": "Une liste d'entiers positifs séparés par des ',' est attendu. Exemple : 20, 25, 30", + "max_opening_degrees_format": "Une liste d'entiers positifs entre 0 et l'ouverture maximale de la vanne séparés par des ',' est attendu. Exemple : 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Pour chaque équipement sous-jacent, max_opening_degrees doit être strictement supérieur à min_opening_degrees. Veuillez vérifier votre configuration.", + "vswitch_configuration_incorrect": "La configuration de la personnalisation des commandes est incorrecte. Elle est obligatoire pour les sous-jacents non switch et le format doit être 'service_name[/attribut:valeur]'. Plus d'informations dans le README." + }, + "abort": { + "already_configured": "L'appareil est déjà configuré" + } + }, + "options": { + "flow_title": "Versatile Thermostat configuration", + "step": { + "user": { + "title": "Type - {name}", + "description": "Choisissez le type de thermostat que vous voulez créer", + "data": { + "thermostat_type": "Type de thermostat" + }, + "data_description": { + "thermostat_type": "Un seul thermostat de type Configuration centrale est possible." + } + }, + "menu": { + "title": "Menu", + "description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.", + "menu_options": { + "main": "Principaux Attributs", + "central_boiler": "Chauffage central", + "type": "Sous-jacents", + "tpi": "Paramètres TPI", + "features": "Fonctions", + "presets": "Pre-réglages", + "window": "Détection d'ouvertures", + "motion": "Détection de mouvement", + "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", + "sync_device_internal_temp": "Synchronisation de la température de l'appareil", + "lock": "Verrouillage", + "heating_failure_detection": "Détection de panne de chauffage", + "finalize": "Finaliser les modifications", + "configuration_not_complete": "Configuration incomplète" + } + }, + "lock": { + "title": "Gestion du Verrouillage - {name}", + "description": "La fonction de verrouillage empêche les modifications d'un thermostat depuis l'interface utilisateur ou les automatisations tout en gardant le thermostat opérationnel.", + "data": { + "use_lock_central_config": "Utiliser la configuration de verrouillage centrale", + "lock_code": "Code pin optionel (4 chiffres)", + "lock_users": "Le verrouillage empêchera les commandes des utilisateurs", + "lock_automations": "Le verrouillage empêchera les commandes des automatisations et des intégrations (ex: planificateur)", + "auto_relock_sec": "Délai de re-verrouillage automatique" + }, + "data_description": { + "use_lock_central_config": "Cochez pour utiliser la configuration de verrouillage centrale. Décochez pour utiliser une configuration de verrouillage spécifique pour ce VTherm", + "lock_code": "Le code PIN pour déverrouiller le thermostat (4 chiffres)", + "auto_relock_sec": "Délai en secondes avant le re-verrouillage automatique après un déverrouillage. Mettre 0 pour désactiver. La valeur par défaut est 30 secondes." + } + }, + "main": { + "title": "Attributs - {name}", + "description": "Principaux attributs obligatoires", + "data": { + "name": "Nom", + "thermostat_type": "Type de thermostat", + "temperature_sensor_entity_id": "Capteur de température", + "last_seen_temperature_sensor_entity_id": "Dernière vue capteur de température", + "external_temperature_sensor_entity_id": "Capteur de température exterieure", + "cycle_min": "Durée du cycle (minutes)", + "temp_min": "Température minimale permise", + "temp_max": "Température maximale permise", + "step_temperature": "Pas de température", + "device_power": "Puissance de l'équipement", + "use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.", + "use_main_central_config": "Utiliser la configuration centrale supplémentaire. Cochez pour utiliser la configuration centrale supplémentaire (température externe, min, max, pas, ...)", + "used_by_controls_central_boiler": "Utilisé par la chaudière centrale. Cochez si ce VTherm doit contrôler la chaudière centrale." + }, + "data_description": { + "cycle_min": "Durée minimale d'un cycle de chauffage/refroidissement en minutes pour éviter des commutations trop fréquentes", + "temp_min": "Température minimale autorisée pour le thermostat", + "temp_max": "Température maximale autorisée pour le thermostat", + "step_temperature": "Pas de température pour le réglage de la température cible", + "temperature_sensor_entity_id": "Id d'entité du capteur de température", + "last_seen_temperature_sensor_entity_id": "Id d'entité du capteur donnant la date et heure de dernière vue capteur de température. L'état doit être au format date heure (ex: 2024-03-31T17:07:03+00:00)", + "external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. Non utilisé si une configuration centrale est définie" + } + }, + "features": { + "title": "Fonctions - {name}", + "description": "Fonctions du thermostat à utiliser", + "data": { + "use_window_feature": "Avec détection des ouvertures", + "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 contrôle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commandent 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", + "use_heating_failure_detection_feature": "Avec détection de panne de chauffage" + } + }, + "type": { + "title": "Entité(s) liée(s) - {name}", + "description": "Attributs de(s) l'entité(s) liée(s)", + "data": { + "underlying_entity_ids": "Les équipements à controller", + "heater_keep_alive": "keep-alive (sec)", + "proportional_function": "Algorithme", + "ac_mode": "AC mode ?", + "sync_device_internal_temp": "Synchronisation de la température interne de l'appareil", + "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": "Compenser la température interne du sous-jacent", + "inverse_switch_command": "Inverser la commande", + "auto_fan_mode": "Auto ventilation mode", + "on_command_text": "Personnalisation des commandes d'allumage", + "vswitch_on_command": "Commande d'allumage (optionnel)", + "off_command_text": "Personnalisation des commandes d'extinction", + "vswitch_off_command": "Commande d'extinction (optionnel)" + }, + "data_description": { + "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 (d'autres algorithmes peuvent être installés sous forme de {plugins_link})", + "ac_mode": "Utilisation du mode Air Conditionné (AC)", + "sync_device_internal_temp": "Synchroniser la température interne de l'appareil sous-jacent avec la température mesurée par le VTherm", + "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": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation (n'est pas nécessaire dans la plupart des cas)", + "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", + "on_command_text": "Pour les sous-jacents de type `select` ou `climate`" + } + }, + "tpi": { + "title": "TPI - {name}", + "description": "Attributs de l'algo Time Proportional Integral", + "data": { + "tpi_coef_int": "coeff_int", + "tpi_coef_ext": "coeff_ext", + "tpi_threshold_low": "TPI seuil bas", + "tpi_threshold_high": "TPI seuil haut", + "minimal_activation_delay": "Délai minimal d'activation", + "minimal_deactivation_delay": "Délai de désactivation minimal", + "use_tpi_central_config": "Utiliser la configuration TPI centrale", + "auto_tpi_mode": "Activer l'apprentissage Auto TPI" + }, + "data_description": { + "tpi_coef_int": "Coefficient à utiliser pour le delta de température interne", + "tpi_coef_ext": "Coefficient à utiliser pour le delta de température externe", + "tpi_threshold_low": "Seuil en ° en dessous duquel le TPI sera activé. 0 signifie pas de seuil", + "tpi_threshold_high": "Seuil en ° au-dessus duquel l'algorithme TPI sera désactivé. 0 signifie pas de seuil", + "use_tpi_central_config": "Cochez pour utiliser la configuration TPI centrale. Décochez et saisissez les attributs pour utiliser une configuration TPI spécifique", + "minimal_activation_delay": "Délai en secondes en-dessous duquel l'équipement ne sera pas activé", + "minimal_deactivation_delay": "Délai en secondes en-dessous duquel l'équipement sera laissé actif", + "auto_tpi_mode": "Les sessions d’apprentissage TPI sont démarrées manuellement ; veuillez lire la doc au préalable." + } + }, + "presets": { + "title": "Pre-réglages - {name}", + "description": "Cochez pour que ce thermostat utilise les pré-réglages de la configuration centrale. Décochez pour utiliser des entités de température spécifiques", + "data": { + "use_presets_central_config": "Utiliser la configuration des pré-réglages centrale" + } + }, + "window": { + "title": "Ouverture - {name}", + "description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'id d'entité vide pour utiliser la détection automatique", + "data": { + "window_sensor_entity_id": "Détecteur d'ouverture (entity id)", + "window_delay": "Délai avant extinction (secondes)", + "window_off_delay": "Délai de prise compte à la fermeture (secondes)", + "window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/heure)", + "window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/heure)", + "window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)", + "use_window_central_config": "Utiliser la configuration centrale des ouvertures", + "window_action": "Action" + }, + "data_description": { + "window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur et pour utiliser la détection automatique", + "window_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte", + "window_off_delay": "Le délai (en secondes) avant que le changement du détecteur soit pris en compte lors de la détection d'une fermeture. Laissez vide pour utiliser le même délai à l'ouveture et à la fermeture", + "window_auto_open_threshold": "Valeur recommandée: entre 3 et 10. 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", + "use_window_central_config": "Cochez pour utiliser la configuration centrale des ouvertures. Décochez et saisissez les attributs pour utiliser une configuration spécifique des ouvertures", + "window_action": "Action a effectuer si la fenêtre est détectée comme ouverte" + } + }, + "motion": { + "title": "Mouvement - {name}", + "description": "Le preset s'ajuste automatiquement si un mouvement est détecté\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser", + "data": { + "motion_sensor_entity_id": "Détecteur de mouvement", + "motion_delay": "Délai d'activation", + "motion_off_delay": "Délai de désactivation", + "motion_preset": "Preset si mouvement", + "no_motion_preset": "Preset sans mouvement", + "use_motion_central_config": "Utiliser la condfiguration centrale du mouvement" + }, + "data_description": { + "motion_sensor_entity_id": "Id d'entité du détecteur de mouvement", + "motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondes)", + "motion_off_delay": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)", + "motion_preset": "Preset à utiliser si mouvement détecté", + "no_motion_preset": "Preset à utiliser si pas de mouvement détecté", + "use_motion_central_config": "Cochez pour utiliser la configuration centrale du mouvement. Décochez et saisissez les attributs pour utiliser une configuration spécifique du mouvement" + } + }, + "power": { + "title": "Puissance - {name}", + "description": "Sélectionne automatiquement le preset 'power' si la puissance consommée est supérieure à un maximum.\nDonnez les entity id des capteurs qui mesurent la puissance totale et la puissance max autorisée.\nEnsuite donnez la puissance de l'équipement.\nTous les capteurs et la puissance consommée par l'équipement doivent avoir la même unité de mesure (kW ou W)", + "data": { + "power_sensor_entity_id": "Puissance totale", + "max_power_sensor_entity_id": "Capteur de puissance Max (entity id)", + "power_temp": "Température si délestaqe", + "use_power_central_config": "Utiliser la configuration centrale de la puissance" + }, + "data_description": { + "power_sensor_entity_id": "Entity id du capteur de puissance totale du logement", + "max_power_sensor_entity_id": "Entity id du capteur de puissance Max autorisée avant délestage", + "power_temp": "Température cible si délestaqe", + "use_power_central_config": "Cochez pour utiliser la configuration centrale de la puissance. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la puissance" + } + }, + "presence": { + "title": "Présence - {name}", + "description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence", + "data": { + "presence_sensor_entity_id": "Capteur de présence", + "use_presence_central_config": "Utiliser la configuration centrale des températures en cas d'absence. Décochez pour avoir des entités de température dédiées" + }, + "data_description": { + "presence_sensor_entity_id": "Id d'entité du capteur de présence" + } + }, + "advanced": { + "title": "Avancés - {name}", + "description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat", + "data": { + "minimal_activation_delay": "Délai minimal d'activation", + "minimal_deactivation_delay": "Délai de désactivation minimal", + "safety_delay_min": "Délai maximal entre 2 mesures de températures", + "safety_min_on_percent": "Pourcentage minimal de puissance", + "safety_default_on_percent": "Pourcentage de puissance a utiliser en mode securité", + "repair_incorrect_state": "Réparer l'état incorrect", + "use_advanced_central_config": "Utiliser la configuration centrale avancée" + }, + "data_description": { + "minimal_activation_delay": "Délai en secondes en-dessous duquel l'équipement ne sera pas activé", + "minimal_deactivation_delay": "Délai en secondes en-dessous duquel l'équipement sera laissé actif", + "safety_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité", + "safety_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé", + "safety_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité", + "repair_incorrect_state": "Réparer automatiquement l'état incorrect des entités sous-jacentes lorsqu'elles sont détectées. La fonction renverra la commande souhaitée si l'état du dispositif sous-jacent ne correspond pas à l'état attendu.", + "use_advanced_central_config": "Cochez pour utiliser la configuration centrale avancée. Décochez et saisissez les attributs pour utiliser une configuration spécifique avancée" + } + }, + "central_boiler": { + "title": "Contrôle de la chaudière centrale - {name}", + "description": "Donnez les services à appeler pour allumer/éteindre la chaudière centrale. Le service à appeler doit être formatté comme suit: `entity_id/service_name[/attribut:valeur]` (/attribut:valeur est facultatif)\nPar exemple:\n- pour allumer un switch: `switch.controle_chaudiere/switch.turn_on`\n- pour éteindre un switch: `switch.controle_chaudiere/switch.turn_off`\n- pour programmer la chaudière sur 25° et ainsi forcer son allumage: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- pour envoyer 10° à la chaudière et ainsi forcer son extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`", + "data": { + "central_boiler_activation_service": "Commande pour allumer", + "central_boiler_deactivation_service": "Commande pour éteindre", + "central_boiler_activation_delay_sec": "Délai d'activation (en secondes)", + "keep_alive_boiler_delay_sec": "Délai du maintien actif (en secondes)" + }, + "data_description": { + "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 éteindre la chaudière centrale au format entity_id/service_name[/attribut:valeur]", + "central_boiler_activation_delay_sec": "Délai en secondes avant d'activer la chaudière centrale après qu'un VTherm ait demandé l'allumage", + "keep_alive_boiler_delay_sec": "Délai en secondes pour relancer périodiquement la commande de la chaudière si le maintien actif est activé. Mettre 0 pour désactiver." + } + }, + "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", + "opening_threshold_degree": "Seuil d'ouverture", + "min_opening_degrees": "Ouvertures minimales", + "max_opening_degrees": "Ouvertures maximales", + "max_closing_degree": "Fermetures maximale" + }, + "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 (d'autres algorithmes peuvent être installés sous forme de {plugins_link})", + "opening_threshold_degree": "Seuil d'ouverture la vanne en dessous de laquelle la vanne est considérée comme devant être fermée (et alors 'max_closing_degree' s'appliquera)", + "min_opening_degrees": "Ouverture minimale pour chaque vanne considérée comme chauffante, séparée par des virgules. Par défaut à 0. Exemple : 20, 25, 30", + "max_opening_degrees": "Degré d'ouverture maximum pour chaque équipement sous-jacent, séparé par des virgules. Par défaut 100. La vanne ne s'ouvrira jamais plus que cette valeur. Exemple: 80, 85, 90", + "max_closing_degree": "Fermeture maximale. La vanne ne sera jamais fermée au-delà de cette valeur. Mettez à 100 pour fermer complètement la vanne" + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - Configuration - {name}", + "description": "Paramètres généraux de l'apprentissage Auto TPI.", + "data": { + "auto_tpi_learning_type": "Type d'apprentissage", + "auto_tpi_heating_rate": "Taux de chauffe ({unit}/h)", + "heater_heating_time": "Temps de chauffe (min)", + "heater_cooling_time": "Temps de refroidissement (min)", + "auto_tpi_aggressiveness": "Agressivité", + "auto_tpi_enable_advanced_settings": "Activer les paramètres avancés", + "auto_tpi_continuous_kext": "Apprentissage continu de Kext" + }, + "data_description": { + "auto_tpi_learning_type": "Choisissez 'Découverte' pour le démarrage initial (Moyenne pondérée, poids 1) ou 'Ajustement fin' pour des ajustements continus (EWMA, Alpha 0.08).", + "auto_tpi_heating_rate": "Capacité de montée en température du radiateur ({unit} par heure). Laisser à 0 pour l'apprentissage automatique (bootstrap).", + "heater_heating_time": "Temps pour atteindre la pleine puissance (minutes)", + "heater_cooling_time": "Temps pour refroidir après arrêt (minutes)\n\n| Type | Temps de chauffe | Temps de refroidissement |\n| :--- | :--- | :--- |\n| Radiateur électrique | 5mn | 7mn |\n| Radiateur à eau | 15mn | 20mn |\n| Chauffage par le sol | 30mn | 45mn |", + "auto_tpi_aggressiveness": "Facteur de réduction des coefficients appris (50-100%). Des valeurs plus basses donnent des coefficients plus conservateurs et réduisent le risque de dépassement de consigne.", + "auto_tpi_enable_advanced_settings": "Cochez pour modifier les paramètres de l'algorithme choisi.", + "auto_tpi_continuous_kext": "Cochez pour activer l'ajustement continu du coefficient extérieur (Kext) hors session AutoTPI." + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - Moyenne pondérée - {name}", + "description": "Paramètres pour la méthode Moyenne pondérée.", + "data": { + "auto_tpi_avg_initial_weight": "Poids Initial" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Poids initial des coefficients existants dans le calcul de la moyenne (1-50 est typique). Plus le poids est élevé, plus l'apprentissage est lent et stable." + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA - {name}", + "description": "Paramétres pour la méthode EWMA.\n\nRecommandations :\n| Situation | Alpha (ema_alpha) | Taux de Décroissance (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Ajustements fort | 0.15 | 0.08 |\n| Ajustement fin | 0.08 | 0.12 |", + "data": { + "auto_tpi_ema_alpha": "Alpha", + "auto_tpi_ema_decay_rate": "Taux de décroissance", + "auto_tpi_continuous_kext_alpha": "Alpha Apprentissage Continu" + }, + "data_description": { + "auto_tpi_ema_alpha": "Facteur de lissage (0-1). Plus élevé = adaptation plus rapide", + "auto_tpi_ema_decay_rate": "Taux auquel Alpha diminue avec le temps (stabilisation)", + "auto_tpi_continuous_kext_alpha": "Facteur de lissage pour l'apprentissage continu (Alpha). Défaut 0.04 (adaptation env. 3-5 jours)." + } + }, + "sync_device_internal_temp": { + "title": "Synchronisation de la température interne des sous-jacents", + "description": "Configuration de la synchronisation des températures internes des sous-jacents", + "data": { + "sync_with_calibration": "Application d'un calibrage", + "sync_entity_ids": "Identifiants des entités utilisées pour la synchronisation" + }, + "data_description": { + "sync_with_calibration": "Cochez pour appliquer le calibrage du décalage lors de la synchronisation de la température interne. Décochez pour copier directement la température vers les entités sélectionnées", + "sync_entity_ids": "La liste des entités utilisées pour synchroniser la température interne des appareils sous-jacents. Il doit y en avoir une par appareil sous-jacent. Doit être une entité de calibrage si la case est cochée ou une entité de température sinon. Les deux sont des entités de type `number`." + } + }, + "heating_failure_detection": { + "title": "Détection de panne de chauffage - {name}", + "description": "Configuration de la détection de panne de chauffage. Cette fonctionnalité détecte les anomalies lorsque le chauffage est demandé mais que la température n'augmente pas, ou lorsque le chauffage est éteint mais que la température continue de monter.", + "data": { + "use_heating_failure_detection_feature": "Activer la détection de panne de chauffage", + "use_heating_failure_detection_central_config": "Utiliser la configuration centrale de détection de panne", + "heating_failure_threshold": "Seuil de panne de chauffage", + "cooling_failure_threshold": "Seuil de panne de refroidissement", + "heating_failure_detection_delay": "Délai de détection (minutes)", + "temperature_change_tolerance": "Tolérance (°C)", + "failure_detection_enable_template": "Template d'activation de la détection" + }, + "data_description": { + "use_heating_failure_detection_feature": "Activer la détection d'anomalies de chauffage pour les VTherms avec TPI", + "use_heating_failure_detection_central_config": "Cochez pour utiliser la configuration centrale de détection de panne. Décochez pour utiliser des paramètres spécifiques à ce VTherm", + "heating_failure_threshold": "Seuil de on_percent au-dessus duquel le chauffage devrait faire monter la température (0.9 = 90%)", + "cooling_failure_threshold": "Seuil de on_percent en-dessous duquel la température ne devrait pas augmenter (0.0 = 0%)", + "heating_failure_detection_delay": "Temps en minutes à attendre avant de vérifier si la température a évolué comme attendu", + "temperature_change_tolerance": "Variation minimale de température en degrés pour être considérée comme significative. Les variations plus petites sont ignorées pour filtrer le bruit des capteurs (défaut: 0.5°C)", + "failure_detection_enable_template": "Un template Jinja2 optionnel qui doit retourner True pour activer la détection. Utile pour désactiver temporairement la détection lorsqu'une source de chaleur externe est active (ex: `is_state('binary_sensor.poele_a_bois', 'off')` entre double accolades). Si vide, la détection est toujours active." + } + } + }, + "error": { + "unknown": "Erreur inattendue", + "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", + "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", + "sync_device_internal_temp_nb_entities_incorrect": "Le nombre d'entités pour la synchronisation de la température interne des appareils sous-jacents doit être égal au nombre d'appareils sous-jacents", + "min_opening_degrees_format": "Une liste d'entiers positifs séparés par des ',' est attendu. Exemple : 20, 25, 30", + "max_opening_degrees_format": "Une liste d'entiers positifs entre 0 et l'ouverture maximale de la vanne séparés par des ',' est attendu. Exemple : 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Pour chaque équipement sous-jacent, max_opening_degrees doit être strictement supérieur à min_opening_degrees. Veuillez vérifier votre configuration.", + "vswitch_configuration_incorrect": "La configuration de la personnalisation des commandes est incorrecte. Elle est obligatoire pour les sous-jacents non switch et le format doit être 'service_name[/attribut:valeur]'. Plus d'informations dans le README." + }, + "abort": { + "already_configured": "L'appareil est déjà configuré" + } + }, + "selector": { + "thermostat_type": { + "options": { + "thermostat_central_config": "Configuration centrale", + "thermostat_over_switch": "Thermostat sur un switch", + "thermostat_over_climate": "Thermostat sur un autre thermostat", + "thermostat_over_valve": "Thermostat sur une vanne" + } + }, + "auto_regulation_mode": { + "options": { + "auto_regulation_slow": "Lente", + "auto_regulation_strong": "Forte", + "auto_regulation_medium": "Moyenne", + "auto_regulation_light": "Légère", + "auto_regulation_expert": "Expert", + "auto_regulation_none": "Aucune", + "auto_regulation_valve": "Contrôle direct de la vanne" + } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "Pas d'auto fan", + "auto_fan_low": "Faible", + "auto_fan_medium": "Moyenne", + "auto_fan_high": "Forte", + "auto_fan_turbo": "Turbo" + } + }, + "window_action": { + "options": { + "window_turn_off": "Eteindre", + "window_fan_only": "Ventilateur seul", + "window_frost_temp": "Hors gel", + "window_eco_temp": "Eco" + } + }, + "presets": { + "options": { + "frost": "Hors-gel", + "eco": "Eco", + "comfort": "Confort", + "boost": "Renforcé (boost)" + } + }, + "auto_start_stop": { + "options": { + "auto_start_stop_none": "Désactivé", + "auto_start_stop_very_slow": "Très lente", + "auto_start_stop_slow": "Lente", + "auto_start_stop_medium": "Medium", + "auto_start_stop_fast": "Rapide" + } + }, + "auto_tpi_calculation_method": { + "options": { + "average": "Moyenne", + "ema": "Moyenne Mobile Exponentielle (EMA)" + } + }, + "auto_tpi_learning_type": { + "options": { + "discovery": "Découverte", + "fine_tuning": "Ajustement fin" + } + } + }, + "entity": { + "climate": { + "versatile_thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "power": "Délestage", + "safety": "Sécurité", + "none": "Manuel", + "frost": "Hors Gel" + } + } + } + } + }, + "binary_sensor": { + "safety_state": { + "name": "État de sécurité" + }, + "heating_failure_state": { + "name": "État d'anomalie de chauffe" + }, + "overpowering_state": { + "name": "État de délestage" + }, + "window_state": { + "name": "État de la fenêtre" + }, + "motion_state": { + "name": "État de mouvement" + }, + "presence_state": { + "name": "État de présence" + }, + "window_bypass_state": { + "name": "Bypass fenêtre" + }, + "central_boiler_state": { + "name": "Chaudière centrale" + } + }, + "number": { + "frost_temp": { + "name": "Hors gel " + }, + "eco_temp": { + "name": "Eco" + }, + "comfort_temp": { + "name": "Confort" + }, + "boost_temp": { + "name": "Boost" + }, + "frost_ac_temp": { + "name": "Hors gel clim" + }, + "eco_ac_temp": { + "name": "Eco clim" + }, + "comfort_ac_temp": { + "name": "Confort clim" + }, + "boost_ac_temp": { + "name": "Boost clim" + }, + "frost_away_temp": { + "name": "Hors gel abs" + }, + "eco_away_temp": { + "name": "Eco abs" + }, + "comfort_away_temp": { + "name": "Confort abs" + }, + "boost_away_temp": { + "name": "Boost abs" + }, + "eco_ac_away_temp": { + "name": "Eco clim abs" + }, + "comfort_ac_away_temp": { + "name": "Confort clim abs" + }, + "boost_ac_away_temp": { + "name": "Boost clim abs" + } + } + }, + "services": { + "set_auto_tpi_mode": { + "name": "Définir le mode Auto TPI", + "description": "Active ou désactive le mode d'apprentissage Auto TPI", + "fields": { + "auto_tpi_mode": { + "name": "Mode Auto TPI", + "description": "Activer (vrai) ou désactiver (faux) l'apprentissage Auto TPI" + }, + "reinitialise": { + "name": "Réinitialiser les données d'apprentissage", + "description": "Réinitialiser toutes les données d'apprentissage lors de l'activation du mode Auto TPI (par défaut vrai)" + } + } + }, + "reset_auto_tpi_capacities": { + "name": "Réinitialiser les Capacités Auto TPI", + "description": "Réinitialise les capacités maximales de chauffage et de refroidissement à 1.0 °C/h. Les coefficients appris ne sont PAS réinitialisés." + }, + "auto_tpi_calibrate_capacity": { + "name": "Auto Tpi Calibrer la Capacité", + "description": "Calibre la capacité de chauffage/refroidissement (en °C/heure) en utilisant la régression linéaire sur les données d'historique de l'entité. Les coefficients TPI appris ne sont PAS mis à jour.", + "fields": { + "start_date": { + "name": "Date de début de l'historique", + "description": "Date à partir de laquelle récupérer l'historique (par défaut il y a 30 jours)" + }, + "end_date": { + "name": "Date de fin de l'historique", + "description": "Date jusqu'à laquelle récupérer l'historique (par défaut maintenant)" + }, + "hvac_mode": { + "name": "Mode HVAC", + "description": "Quel mode calibrer (heat ou cool)." + }, + "save_to_config": { + "name": "Sauvegarder dans la configuration", + "description": "Appliquer la capacité calculée à la configuration Auto TPI" + }, + "min_power_threshold": { + "name": "Seuil de puissance minimum", + "description": "Pourcentage de puissance minimum pour considérer un cycle comme saturé (par défaut 95%). Des valeurs plus basses incluront plus de cycles mais peuvent être moins précises." + } + } + }, + "set_tpi_parameters": { + "name": "Définir les paramètres TPI", + "description": "Modifier les paramètres TPI", + "fields": { + "minimal_activation_delay": { + "name": "Délai minimal d'activation", + "description": "Délai en secondes en-dessous duquel l'équipement ne sera pas activé" + }, + "minimal_deactivation_delay": { + "name": "Délai de désactivation minimal", + "description": "Délai en secondes en-dessous duquel l'équipement sera laissé actif" + }, + "tpi_threshold_low": { + "name": "Seuil TPI bas", + "description": "Seuil en ° en dessous duquel le TPI sera activé. 0 signifie pas de seuil" + }, + "tpi_threshold_high": { + "name": "Seuil TPI haut", + "description": "Seuil en ° au-dessus duquel l'algorithme TPI sera désactivé. 0 signifie pas de seuil" + } + } + }, + "lock": { + "name": "Verrouiller", + "description": "Verrouille le thermostat, empêchant toute modification de sa configuration depuis l'interface utilisateur ou les automatisations.", + "fields": { + "code": { + "name": "Code de verrouillage", + "description": "Le code de verrouillage optionnel" + } + } + }, + "unlock": { + "name": "Déverrouiller", + "description": "Déverrouille le thermostat, autorisant les modifications de sa configuration.", + "fields": { + "code": { + "name": "Code de verrouillage", + "description": "Le code de verrouillage optionnel" + } + } + }, + "download_logs": { + "name": "Télécharger les logs", + "description": "Collecte et télécharge les logs filtrés pour une entité VTherm.", + "fields": { + "log_level": { + "name": "Niveau de log", + "description": "Niveau de log minimum à inclure" + }, + "period_start": { + "name": "Début de la période", + "description": "Début de la période d'extraction" + }, + "period_end": { + "name": "Fin de la période", + "description": "Fin de la période d'extraction" + } + } + } + }, + "exceptions": { + "auto_tpi_learning_stopped": { + "message": "L'apprentissage Auto TPI pour {name} a été arrêté car 3 échecs consécutifs ont été détectés. Raison : {reason}. Veuillez vérifier votre configuration." + } + } +} \ No newline at end of file diff --git a/custom_components/versatile_thermostat/translations/it.json b/custom_components/versatile_thermostat/translations/it.json new file mode 100644 index 0000000..c00b365 --- /dev/null +++ b/custom_components/versatile_thermostat/translations/it.json @@ -0,0 +1,1051 @@ +{ + "title": "Configurazione Versatile Thermostat", + "config": { + "flow_title": "Configurazione Versatile Thermostat", + "step": { + "user": { + "title": "Tipo di Versatile Thermostat", + "description": "Scegliere il tipo di termostato che si desidera creare", + "data": { + "thermostat_type": "Tipo di termostato" + }, + "data_description": { + "thermostat_type": "È possibile un solo tipo di configurazione centralizzata." + } + }, + "menu": { + "title": "Menu", + "description": "Configurare il termostato. Una volta inseriti tutti i parametri richiesti, sarà possibile completare la configurazione.", + "menu_options": { + "main": "Attributi principali", + "central_boiler": "Caldaia centralizzata", + "type": "Sottostanti", + "tpi": "Parametri TPI", + "features": "Carateristiche", + "presets": "Preimpostazioni", + "window": "Rilevamento finestra aperta", + "motion": "Rilevamento movimento", + "power": "Gestione energetica", + "presence": "Rilevata Presenza", + "advanced": "Parametri avanzati", + "auto_start_stop": "Auto start and stop", + "valve_regulation": "Configurazione della regolazione della valvola", + "sync_device_internal_temp": "Sincronizza la temperatura del dispositivo", + "lock": "Blocco", + "heating_failure_detection": "Rilevamento guasto riscaldamento", + "auto_tpi_configuration": "Auto TPI - Configurazione", + "auto_tpi_avg_settings": "Auto TPI - Media ponderata", + "auto_tpi_ema_settings": "Auto TPI - EWMA", + "finalize": "Fatto", + "configuration_not_complete": "Configurazione non completata" + } + }, + "lock": { + "title": "Gestione blocco", + "description": "La funzione di blocco impedisce modifiche alla configurazione di un termostato dall'interfaccia utente o dalle automazioni mantenendo il termostato operativo.", + "data": { + "use_lock_central_config": "Utilizzare la configurazione di blocco centrale", + "lock_code": "PIN opzionale (4 cifre)", + "lock_users": "Il blocco impedirà i comandi degli utenti", + "lock_automations": "Il blocco impedirà i comandi delle automazioni e delle integrazioni (ad es. pianificatore)", + "auto_relock_sec": "Ritardo di riattivazione automatica del blocco" + }, + "data_description": { + "use_lock_central_config": "Selezionare per utilizzare la configurazione di blocco centrale. Deselezionare per utilizzare una configurazione di blocco specifica per questo VTherm", + "lock_code": "Il codice PIN per sbloccare il termostato (4 cifre)", + "auto_relock_sec": "Tempo in secondi prima del ri-blocco automatico dopo uno sblocco. Impostare 0 per disattivare. Il valore predefinito è 30 secondi." + } + }, + "main": { + "title": "Nuovo Versatile Thermostat", + "description": "Principali attributi obbligatori", + "data": { + "name": "Nome", + "thermostat_type": "Tipo di termostato", + "temperature_sensor_entity_id": "Temperatura ambiente", + "last_seen_temperature_sensor_entity_id": "Ultima visualizzazione data e ora temperatura ambiente", + "external_temperature_sensor_entity_id": "Sensore di temperatura esterna", + "cycle_min": "Durata ciclo (minuti)", + "temp_min": "Temperatura minima permessa", + "temp_max": "Temperatura massima permessa", + "step_temperature": "Passi di temperatura", + "device_power": "Potenza del dispositivo", + "use_central_mode": "Abilita il controllo tramite entità centrale (richiede configurazione centralizzata). Seleziona per abilitare il controllo di VTherm con le entità central_mode selezionate.", + "use_main_central_config": "Utilizzare configurazione principale centralizzata aggiuntiva. Selezionare per utilizzare la configurazione principale centrale (temperatura esterna, min, max, passo, ...).", + "used_by_controls_central_boiler": "Utilizzato dalla caldaia centralizzata. Selezionare se questo VTherm deve avere il controllo sulla caldaia centralizzata." + }, + "data_description": { + "cycle_min": "Durata minima di un ciclo di riscaldamento/raffreddamento in minuti per evitare commutazioni troppo frequenti", + "temp_min": "Temperatura minima consentita per il termostato", + "temp_max": "Temperatura massima consentita per il termostato", + "step_temperature": "Passo di temperatura per l'impostazione della temperatura target", + "temperature_sensor_entity_id": "Entity id del sensore di temperatura ambiente", + "last_seen_temperature_sensor_entity_id": "Ultima visualizzazione entity id del sensore di temperatura ambiente. Dovrebbe essere il sensore data e ora", + "external_temperature_sensor_entity_id": "Entity id del sensore di temperatura esterna. Non utilizzato se è selezionata la configurazione centralizzata" + } + }, + "features": { + "title": "Carateristiche", + "description": "Caratteristiche del termostato", + "data": { + "use_window_feature": "Usa il rilevamento finestra aperta", + "use_motion_feature": "Usa rilevamento del movimento", + "use_power_feature": "Usa Gestione energia", + "use_presence_feature": "Usa rilevamento presenza", + "use_central_boiler_feature": "Utilizza una caldaia centralizzata. Selezionare per aggiungere un controllo alla caldaia centralizzata. Dopo aver selezionato questa casella di controllo, occorre configurare il VTherm che avrà il controllo della caldaia centralizzata. Se un VTherm richiede il riscaldamento, la caldaia verrà accesa. Se nessun VTherm richiede il riscaldamento, la caldaia verrà spenta. I comandi per accendere/spegnere la caldaia centralizzata sono riportati nella relativa pagina di configurazione.", + "use_auto_start_stop_feature": "Usa la funzione auto start and stop", + "use_heating_failure_detection_feature": "Usa rilevamento guasti riscaldamento" + } + }, + "type": { + "title": "Entità collegate", + "description": "Attributi delle entità collegate", + "data": { + "underlying_entity_ids": "Dispositivo(i) da controllare", + "heater_keep_alive": "intervallo di mantenimento della connessione in secondi", + "proportional_function": "Algoritmo", + "ac_mode": "Modo AC ?", + "sync_device_internal_temp": "Sincronizzare la temperatura interna del dispositivo", + "auto_regulation_mode": "Auto-regolazione", + "auto_regulation_dtemp": "Regulazione soglia", + "auto_regulation_periode_min": "Periodo minimo di regolazione", + "auto_regulation_use_device_temp": "Utilizzare la temperatura interna del sottostante", + "inverse_switch_command": "Invertire il comando di commutazione", + "auto_fan_mode": " Modo Auto fan", + "on_command_text": "Personalizzazione comando di accensione", + "vswitch_on_command": "Comando di accensione opzionale", + "off_command_text": "Personalizzazione comando di spegnimento", + "vswitch_off_command": "Comando di spegnimento opzionale" + }, + "data_description": { + "underlying_entity_ids": "Dispositivo(i) da controllare - è richiesto almeno 1", + "heater_keep_alive": "Intervallo di aggiornamento dello stato dell'interruttore del riscaldatore opzionale. Lasciare vuoto se non richiesto.", + "proportional_function": "Algoritmo da utilizzare (altri algoritmi possono essere installati come {plugins_link})", + "ac_mode": "Utilizzare la modalità Aria condizionata (AC)", + "sync_device_internal_temp": "Configurazione per sincronizzare la temperatura interna dei dispositivi sottostanti con le entità selezionate", + "auto_regulation_mode": "Correzione automatica della temperatura desiderata", + "auto_regulation_dtemp": "La soglia in ° (o % per la valvola) al di sotto della quale la variazione di temperatura non verrà inviata", + "auto_regulation_periode_min": "Durata in minuti tra due aggiornamenti della regolazione", + "auto_regulation_use_device_temp": "Utilizzare l'eventuale sensore di temperatura interno del sottostante per accelerare l'autoregolazione (non necessario nella maggior parte dei casi)", + "inverse_switch_command": "Per gli interruttori con filo pilota e diodo potrebbe essere necessario invertire il comando", + "auto_fan_mode": "Attivare automaticamente la ventilazione quando è necessario un riscaldamento/raffreddamento intenso", + "on_command_text": "Per i sottostanti di tipo “select” o “climate” è necessario personalizzare i comandi." + } + }, + "tpi": { + "title": "TPI", + "description": "Attributi dell'integrale proporzionale al tempo (TPI)", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "tpi_threshold_low": "Soglia minima TPI", + "tpi_threshold_high": "Soglia massima TPI", + "minimal_activation_delay": "Ritardo minimo di attivazione", + "minimal_deactivation_delay": "Ritardo minimo di disattivazione", + "use_tpi_central_config": "Utilizzare la configuration TPI centralizzata", + "auto_tpi_mode": "Abilita Auto apprendimento TPI" + }, + "data_description": { + "tpi_coef_int": "Coefficiente da utilizzare per il delta della temperatura interna", + "tpi_coef_ext": "Coefficiente da utilizzare per il delta della temperatura esterna", + "tpi_threshold_low": "Soglia in ° al di sotto della quale l'algoritmo TPI sarà attivo. 0 significa nessuna soglia", + "tpi_threshold_high": "Soglia in ° al di sopra della quale l'algoritmo TPI sarà disattivo. 0 significa nessuna soglia", + "use_tpi_central_config": "Selezionare per utilizzare la configurazione TPI centralizzata. Deselezionare per utilizzare una configurazione TPI specifica per questo VTherm", + "minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata", + "minimal_deactivation_delay": "Ritardo in secondi durante il quale l'apparecchiatura rimarrà attiva", + "auto_tpi_mode": "Le sessioni di apprendimento TPI vengono avviate manualmente, leggere prima la documentazione." + } + }, + "presets": { + "title": "Preimpostazioni", + "description": "Selezionare se il termostato utilizzerà le impostazioni centralizzate predefinite - deselezionare per consentire al termostato di avere le proprie impostazioni predefinite", + "data": { + "use_presets_central_config": "Utilizzare preimpostazioni centralizzate" + } + }, + "window": { + "title": "Gestione finestra aperta", + "description": "Gestione finestra aperta.\nÈ possibile configurare il rilevamento automatico dell'apertura delle finestre in base alla diminuzione della temperatura", + "data": { + "window_sensor_entity_id": "Entity id del sensore finestra", + "window_delay": "Ritardo di attivazione sensore finestra (secondi)", + "window_off_delay": "Ritardo di disattivazione sensore finestra (secondi)", + "window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico dell'apertura della finestra (in °/ora)", + "window_auto_close_threshold": "Soglia di aumento della temperatura per il rilevamento automatico della chiusura della finestra (in °/ora)", + "window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min.)", + "use_window_central_config": "Uitilizzare Configurazione centralizzata della finestra", + "window_action": "Azione" + }, + "data_description": { + "window_sensor_entity_id": "Lasciare vuoto se non si desidera utilizzare alcun sensore finestra e si desidera utilizzare il rilevamento automatico", + "window_delay": "Rtardo in secondi prima che lo stato “on” del sensore venga preso in considerazione", + "window_off_delay": "Rtardo in secondi prima che lo stato “off\" del sensore venga preso in considerazione. Lasciare vuoto per utilizzare lo stesso valore della stato \"on\"", + "window_auto_open_threshold": "Valore consigliato: tra 3 e 10. Lasciare vuoto se non si utilizza il rilevamento automatico dell'apertura della finestra", + "window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se non si utilizza il rilevamento automatico dell'apertura della finestra", + "window_auto_max_duration": "Valore consigliato: 60 (1 ora). Lasciare vuoto se non si utilizza il rilevamento automatico dell'apertura della finestra", + "use_window_central_config": "Selezionare per utilizzare la configurazione centralizzata della finestra. Deselezionare per utilizzare una configurazione della finestra specifica per questo VTherm", + "window_action": "Azione da eseguire se la finestra viene rilevata come aperta" + } + }, + "motion": { + "title": "Gestione movimento", + "description": "Gestione del sensore di movimento. L'impostazione predefinita può cambiare automaticamente in base al rilevamento del movimento\nPreimpostazione movimento e Preimpostazione no-movimento devono essere impostati sul nome del preset corrispondente", + "data": { + "motion_sensor_entity_id": "Entity id del sensore di movimento", + "motion_delay": "Ritardo di attivazione", + "motion_off_delay": "Ritardo di disattivazione", + "motion_preset": "Preimpostazione movimento", + "no_motion_preset": "Preimpostazione no-movimento", + "use_motion_central_config": "Usare configurazione centralizzata del movimento" + }, + "data_description": { + "motion_sensor_entity_id": "L'entity id del sensore di movimento", + "motion_delay": "Ritardo di attivazione dovuto al movimento (secondi)", + "motion_off_delay": "Ritardo di disattivazione dovuto al mancato movimento (secondi)", + "motion_preset": "Preimpostazione da utilizzare quando viene rilevato un movimento", + "no_motion_preset": "Preimpostazione da utilizzare quando non viene più rilevato movimento", + "use_motion_central_config": "Selezionare per utilizzare la configurazione centralizzata del movimento. Deselezionare per utilizzare una configurazione specifica per questo VTherm." + } + }, + "power": { + "title": "Gestione energetica", + "description": "Attributi di gestione dell'eneregia elettrica.\nIndica la potenza e il sensore della potenza massima della tua abitazione.\nSpecifica il consumo energetico del riscaldatore quando è acceso.\nTutti i sensori e l'alimentazione dei dispositivi devono utilizzare la stessa unità di misura (kW o W)", + "data": { + "power_sensor_entity_id": "Potenza", + "max_power_sensor_entity_id": "Potenza massima", + "power_temp": "Distribuzione Temperatura", + "use_power_central_config": "Utilizzare configurazione centralizzata della potenza" + }, + "data_description": { + "power_sensor_entity_id": "Entity id del sensore di potenza", + "max_power_sensor_entity_id": "Entity id del ensore della potenza massima", + "power_temp": "Temperature per la distribuzione della potenza", + "use_power_central_config": "Selezionare per utilizzare la configurazione centralizzata della potenza. Deselezionare per utilizzare una configurazione specifica per questo VTherm." + } + }, + "presence": { + "title": "Gestione presenza", + "description": "Attributi di gestione della presenza.\nFornisce un sensore di presenza della tua casa (vero se qualcuno è presente) e fornisce l'impostazione predefinita della temperatura corrispondente", + "data": { + "presence_sensor_entity_id": "Sensore di presenza", + "use_presence_central_config": "Utilizza la configurazione centralizzata della temperatura in presenza. Deseleziona per utilizzare entità di temperatura specifiche." + }, + "data_description": { + "presence_sensor_entity_id": "Entity id del sensore di presenza" + } + }, + "advanced": { + "title": "Parametri avanzati", + "description": "Configurazione dei parametri avanzati. Lascia i valori predefiniti se non sai cosa stai facendo.\nQuesti parametri possono causare un controllo della temperatura molto scarso o una cattiva regolazione della potenza", + "data": { + "safety_delay_min": "Ritardo di sicurezza (in minuti)", + "safety_min_on_percent": "Percentuale minima di potenza per attivare la modalità di sicurezza", + "safety_default_on_percent": "Percentuale di potenza da utilizzare in modalità di sicurezza", + "repair_incorrect_state": "Ripara lo stato errato", + "use_advanced_central_config": "Utilizza la configurazione avanzata centrale" + }, + "data_description": { + "safety_delay_min": "Ritardo massimo consentito in minuti tra due misurazioni della temperatura. Superato questo ritardo, il termostato passerà allo stato di spegnimento di sicurezza", + "safety_min_on_percent": "Valore minimo percentuale di riscaldamento per l'attivazione della preimpostazione di sicurezza. Al di sotto di tale percentuale di potenza, il termostato non entrerà nella preimpostazione di sicurezza", + "safety_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nell'impostazione di sicurezza. Impostare su 0 per spegnere il riscaldatore nell'impostazione di sicurezza.", + "repair_incorrect_state": "Ripara automaticamente lo stato errato delle entità sottostanti quando rilevato. La funzione reinvierà il comando desiderato se lo stato del dispositivo sottostante non corrisponde allo stato previsto.", + "use_advanced_central_config": "Selezionare per utilizzare la configurazione avanzata centrale. Deselezionare per utilizzare una configurazione avanzata specifica per questo VTherm." + } + }, + "central_boiler": { + "title": "Controllo della caldaia centralizzata", + "description": "Inserisci i servizi da chiamare per accendere/spegnere la caldaia centralizzata. Il servizio chiamato deve essere formattato come segue: `entity_id/nome_servizi[/attributo:valore]` (/attributo:valore è facoltativo)\nAd esempio:\n- per accendere un interruttore: `switch.controle_chaudiere/switch.turn_on`\n- per spegnere un interruttore: `switch.controle_chaudiere/switch.turn_off`\n- per programmare la caldaia a 25° e quindi forzarne l'accensione: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- per inviare 10° alla caldaia e quindi forzarne lo spegnimento: `climate. thermostat_chaudiere/climate.set_temperature/temperature:10`", + "data": { + "central_boiler_activation_service": "Comando per accendere", + "central_boiler_deactivation_service": "Comando per spegnere", + "central_boiler_activation_delay_sec": "Ritardo di attivazione (in secondi)", + "keep_alive_boiler_delay_sec": "Ritardo keep-alive (secondi)" + }, + "data_description": { + "central_boiler_activation_service": "Comando per accendere la caldaia centralizzata formattato come entity_id/nome_servizio[/attributo:valore]", + "central_boiler_deactivation_service": "Comando per spegnere la caldaia centralizzata formattato come entity_id/nome_servizio[/attributo:valore]", + "central_boiler_activation_delay_sec": "Ritardo in secondi prima di attivare la caldaia centralizzata dopo che un VTherm ha richiesto l'accensione", + "keep_alive_boiler_delay_sec": "Ritardo in secondi per inviare periodicamente il comando della caldaia se keep-alive è abilitato. Impostare su 0 per disabilitare keep-alive." + } + }, + "valve_regulation": { + "title": "Auto-regolazione con valvola", + "description": "Configurazione per l'autoregolazione con controllo diretto della valvola", + "data": { + "offset_calibration_entity_ids": "Entità di calibrazione offset", + "opening_degree_entity_ids": "Entità del grado di apertura", + "closing_degree_entity_ids": "Entità del grado di chiusura", + "proportional_function": "Algoritmo", + "min_opening_degrees": "Gradi di apertura minima", + "opening_threshold_degree": "Soglia di apertura", + "max_opening_degrees": "Grado di apertura massimo", + "max_closing_degree": "Grado di chiusura massimo" + }, + "data_description": { + "offset_calibration_entity_ids": "Elenco delle entità di “calibrazione dell'offset”. Impostalo se il tuo TRV dispone dell'entità per una migliore regolazione. Dovrebbe essercene una per ogni entità climatica sottostante.", + "opening_degree_entity_ids": "Elenco delle entità “grado di apertura”. Dovrebbe essercene una per ciascuna entità climatica sottostante.", + "closing_degree_entity_ids": "Elenco delle entità “grado di chiusura”. Impostarlo se il TRV dispone dell'entità per una migliore regolazione. Dovrebbe essercene uno per ogni entità climatica sottostante", + "proportional_function": "Algoritmo da utilizzare (altri algoritmi possono essere installati come {plugins_link})", + "opening_threshold_degree": "Valore di apertura della valvola al di sotto del quale la valvola deve essere considerata chiusa (e quindi si applicherà 'max_closing_degree')", + "min_opening_degrees": "Valore minimo del grado di apertura per ciascun dispositivo sottostante, separato da virgola. Il valore predefinito è 0. Esempio: 20, 25, 30", + "max_opening_degrees": "Valore massimo del grado di apertura per ogni dispositivo sottostante, separato da virgole. Il valore predefinito è 100. La valvola non si aprirà mai più di questo valore. Esempio: 80, 85, 90", + "max_closing_degree": "Valore massimo del grado di chiusura. La valvola non sarà mai chiusa più di questo valore. Impostare a 100 per chiudere completamente la valvola" + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - Configurazione", + "description": "Impostazioni generali per l'AUTO apprendimento TPI.", + "data": { + "auto_tpi_learning_type": "Tipo di apprendimento", + "heater_heating_time": "Tempo riscaldamento (min)", + "heater_cooling_time": "Tempo raffrescamento (min)", + "auto_tpi_heating_rate": "Velocità di riscaldamento ({unit}/h)", + "auto_tpi_aggressiveness": "Aggressività", + "auto_tpi_enable_advanced_settings": "Abilita paramnetri avanzati", + "auto_tpi_continuous_kext": "Apprendimento continuo di Kext" + }, + "data_description": { + "auto_tpi_learning_type": "Scegliere “Discovery” per l'avvio iniziale (media ponderata, peso 1) o “Regolazione fine” per le regolazioni continue (EWMA, Alpha 0,08).", + "heater_heating_time": "Tempo necessario per raggiungere la piena potenza (minuti)", + "heater_cooling_time": "Tempo di raffreddamento dopo l'arresto (minuti)\n\n| Tipo | Tempo di riscaldamento | Tempo di raffreddamento |\n| :--- | :--- | :--- |\n| Radiatore elettrico | 5 min | 7 min |\n| Radiatore ad acqua | 15 min | 20 min |\n| Riscaldamento a pavimento | 30 min | 45 min |", + "auto_tpi_heating_rate": "Capacità di aumento della temperatura del radiatore ({unit} all'ora). Lasciare a 0 per l'apprendimento automatico (bootstrap).", + "auto_tpi_aggressiveness": "Fattore di riduzione per i coefficienti appresi (50-100%). Valori inferiori determinano coefficienti più conservativi e riducono il rischio di sovra-riscaldamento.", + "auto_tpi_enable_advanced_settings": "Controlla per accedere alla regolazione fine dei parametri (Alpha, Decadimento, Peso).", + "auto_tpi_continuous_kext": "Selezionare per attivare la regolazione continua del coefficiente esterno (Kext) al di fuori della sessione AutoTPI." + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - Media ponderata", + "description": "Parametri per il metodo della media ponderata.", + "data": { + "auto_tpi_avg_initial_weight": "Peso iniziale" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Peso del valore iniziale/precedente nel calcolo della media" + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA", + "description": "Parametri per il metodo EWMA.\n\nRaccomandazioni:\n| Situazione | Alpha (ema_alpha) | Tasso di decadimento (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Apprendimento iniziale | 0,15 | 0,08 |\n| Apprendimento fine | 0,08 | 0,12 |\n| Apprendimento continuo | 0,05 | 0,02 |", + "data": { + "auto_tpi_ema_alpha": "Alpha", + "auto_tpi_ema_decay_rate": "Tasso di decadimento", + "auto_tpi_continuous_kext_alpha": "Alpha Apprendimento continuo" + }, + "data_description": { + "auto_tpi_ema_alpha": "Fattore di livellamento (0-1). Più alto = adattamento più rapido", + "auto_tpi_ema_decay_rate": "Tasso di diminuzione di Alpha nel tempo (stabilizzazione)", + "auto_tpi_continuous_kext_alpha": "Fattore di livellamento per l'apprendimento continuo (Alpha). Impostazione predefinita 0,04 (adattamento circa 3-5 giorni)." + } + }, + "sync_device_internal_temp": { + "title": "Sincronizza la temperatura interna del dispositivo", + "description": "Configurazione per sincronizzare la temperatura interna dei dispositivi sottostanti con entità selezionate", + "data": { + "sync_with_calibration": "Applicare la calibrazione dell'offset", + "sync_entity_ids": "Entity ID utilizzati per la sincronizzazione" + }, + "data_description": { + "sync_with_calibration": "Selezionare per applicare la calibrazione dell'offset durante la sincronizzazione della temperatura interna. Deselezionare per copiare direttamente la temperatura dagli elementi selezionati.", + "sync_entity_ids": "L'elenco delle entità utilizzate per sincronizzare la temperatura interna dei dispositivi sottostanti. Dovrebbe essercene una per ogni dispositivo sottostante. Dovrebbe essere un'entità di calibrazione se la casella di controllo è selezionata, altrimenti un'entità di temperatura. Entrambe sono entità `numero`." + } + }, + "heating_failure_detection": { + "title": "Rilevamento guasti riscaldamento", + "description": "Configura la funzione di rilevamento guasti riscaldamento. Rileva anomalie quando il riscaldamento è previsto ma la temperatura non aumenta, o quando il riscaldamento è spento ma la temperatura continua a salire.", + "data": { + "use_heating_failure_detection_feature": "Abilita rilevamento guasti riscaldamento", + "use_heating_failure_detection_central_config": "Usa configurazione centrale per rilevamento guasti", + "heating_failure_threshold": "Soglia guasto riscaldamento", + "cooling_failure_threshold": "Soglia guasto raffreddamento", + "heating_failure_detection_delay": "Ritardo rilevamento (minuti)", + "temperature_change_tolerance": "Tolleranza variazione temperatura (°C)", + "failure_detection_enable_template": "Template abilitazione rilevamento" + }, + "data_description": { + "use_heating_failure_detection_feature": "Abilita il rilevamento di anomalie di riscaldamento per VTherm con TPI", + "use_heating_failure_detection_central_config": "Seleziona per usare la configurazione centrale di rilevamento guasti. Deseleziona per usare parametri specifici per questo VTherm", + "heating_failure_threshold": "Soglia percentuale di accensione sopra la quale il riscaldamento dovrebbe causare un aumento di temperatura (0.9 = 90%)", + "cooling_failure_threshold": "Soglia percentuale di accensione sotto la quale la temperatura non dovrebbe aumentare (0.0 = 0%)", + "heating_failure_detection_delay": "Tempo in minuti da attendere prima di verificare se la temperatura è cambiata come previsto", + "temperature_change_tolerance": "Variazione minima di temperatura in gradi da considerare significativa. Variazioni minori vengono ignorate per filtrare il rumore del sensore (predefinito: 0.5°C)", + "failure_detection_enable_template": "Un template Jinja2 opzionale che deve restituire True per abilitare il rilevamento. Utile per disabilitare temporaneamente il rilevamento quando una fonte di calore esterna è attiva (es., `is_state('binary_sensor.stufa_legna', 'off')` tra doppie parentesi graffe). Se vuoto, il rilevamento è sempre abilitato." + } + } + }, + "error": { + "unknown": "Errore inatteso", + "unknown_entity": "entity id sconosciuto", + "window_open_detection_method": "È necessario utilizzare un solo metodo di rilevamento delle finestre aperte. Utilizzare il sensore della finestra o il rilevamento automatico tramite soglia di temperatura, ma non entrambi.", + "no_central_config": "Non è possibile selezionare “usa configurazione centralizzata” perché non è stata trovata alcuna configurazione centralizzata. È necessario creare un Versatile Thermostat di tipo “Configurazione centralizzata” per poterlo utilizzare.", + "service_configuration_format": "Il formato della configurazione del servizio è errato", + "valve_regulation_nb_entities_incorrect": "Il numero di entità della valvola per la regolazione delle valvole deve essere pari al numero dei sottostanti", + "sync_device_internal_temp_nb_entities_incorrect": "Il numero di entità per sincronizzare la temperatura interna del dispositivo dovrebbe essere uguale al numero dei sottostanti", + "min_opening_degrees_format": "È richiesto un elenco di numeri interi positivi separati da virgola. Esempio: 20, 25, 30", + "max_opening_degrees_format": "È previsto un elenco di numeri interi positivi tra 0 e 100 separati da virgola. Esempio: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Per ogni dispositivo sottostante, max_opening_degrees deve essere strettamente maggiore di min_opening_degrees. Verificare la configurazione.", + "vswitch_configuration_incorrect": "La configurazione della personalizzazione dei comandi non è corretta. È obbligatoria per i sottostanti non switch e il formato deve essere ‘nome_servizio[/attributo:valore]’. Maggiori informazioni nel README." + }, + "abort": { + "already_configured": "Il dispositivo è gia configurato" + } + }, + "options": { + "flow_title": "Configurazione Versatile Thermostat", + "step": { + "user": { + "title": "Tipo - {name}", + "description": "Scegliere il tipo di termostato che si desidera creare", + "data": { + "thermostat_type": "Tipo di termostato" + }, + "data_description": { + "thermostat_type": "È possibile un solo tipo di configurazione centralizzata." + } + }, + "menu": { + "title": "Menu", + "description": "Configurare il termostato. Una volta inseriti tutti i parametri richiesti, sarà possibile completare la configurazione.", + "menu_options": { + "main": "Attributi principali", + "central_boiler": "Caldaia centralizzata", + "type": "Sottostanti", + "tpi": "Parametri TPI", + "features": "Carateristiche", + "presets": "Preimpostazioni", + "window": "Rilevamento finestra aperta", + "motion": "Rilevamento movimento", + "power": "Gestione energetica", + "presence": "Rilevamento della presenza", + "advanced": "Parametri avanzati", + "auto_start_stop": "Auto start and stop", + "valve_regulation": "Configurazione della regolazione della valvola", + "sync_device_internal_temp": "Sincronizza la temperatura del dispositivo", + "lock": "Gestione del blocco", + "heating_failure_detection": "Rilevamento guasti al riscaldamento", + "finalize": "Finalizza le modifiche", + "configuration_not_complete": "Configurazione non completata" + } + }, + "lock": { + "title": "Gestione blocco - {name}", + "description": "La funzione di blocco impedisce le modifiche a un termostato dall'interfaccia utente o dalle automazioni mantenendo il termostato operativo.", + "data": { + "use_lock_central_config": "Utilizzare la configurazione di blocco centrale", + "lock_code": "PIN opzionale (4 cifre)", + "lock_users": "Il blocco impedirà i comandi degli utenti", + "lock_automations": "Il blocco impedirà i comandi delle automazioni e delle integrazioni (ad es. pianificatore)", + "auto_relock_sec": "Ritardo di riattivazione automatica del blocco" + }, + "data_description": { + "use_lock_central_config": "Selezionare per utilizzare la configurazione di blocco centrale. Deselezionare per utilizzare una configurazione di blocco specifica per questo VTherm", + "lock_code": "Il codice PIN per sbloccare il termostato (4 cifre)", + "auto_relock_sec": "Tempo in secondi prima del ri-blocco automatico dopo uno sblocco. Impostare 0 per disattivare. Il valore predefinito è 30 secondi." + } + }, + "main": { + "title": "Principale - {name}", + "description": "Principali attributi obbligatori", + "data": { + "name": "Nome", + "thermostat_type": "Tipo di termostato", + "temperature_sensor_entity_id": "Temperatura ambiente", + "last_seen_temperature_sensor_entity_id": "Ultima visualizzazione data e ora temperatura ambiente", + "external_temperature_sensor_entity_id": "Entity id del sensore di temperatura esterna", + "cycle_min": "Durata ciclo (minuti)", + "temp_min": "Temperatura minima permessa", + "temp_max": "Temperatura massima permessa", + "step_temperature": "Passi di temperatura", + "device_power": "Potenza del dispositivo", + "use_central_mode": "Abilita il controllo tramite entità centralizzata (richiede configurazione centralizzata). Seleziona per abilitare il controllo di VTherm con le entità central_mode selezionate.", + "use_main_central_config": "Utilizzare configurazione principale centralizzata aggiuntiva. Selezionare per utilizzare la configurazione principale centralizzata (temperatura esterna, min, max, passo, ...).", + "used_by_controls_central_boiler": "Utilizzato dalla caldaia centralizzata. Selezionare se questo VTherm deve avere il controllo sulla caldaia centralizzata." + }, + "data_description": { + "cycle_min": "Durata minima di un ciclo di riscaldamento/raffreddamento in minuti per evitare commutazioni troppo frequenti", + "temp_min": "Temperatura minima consentita per il termostato", + "temp_max": "Temperatura massima consentita per il termostato", + "step_temperature": "Passo di temperatura per l'impostazione della temperatura target", + "temperature_sensor_entity_id": "Entity id del sensore di temperatura ambiente", + "last_seen_temperature_sensor_entity_id": "Ultima visualizzazione entity id del sensore di temperatura ambiente. Dovrebbe essere il sensore data e ora", + "external_temperature_sensor_entity_id": "Entity id del sensore di temperatura esterna. Non utilizzato se è selezionata la configurazione centralizzata" + } + }, + "features": { + "title": "Carateristiche - {name}", + "description": "Caratteristiche del termostato", + "data": { + "use_window_feature": "Utilizzare il rilevamento finestra aperta", + "use_motion_feature": "Utilizzare rilevamento del movimento", + "use_power_feature": "Utilizzare gestione energia", + "use_presence_feature": "Usare rilevamento presenza", + "use_central_boiler_feature": "Utilizza una caldaia centralizzata. Selezionare questa opzione per aggiungere un controllo alla caldaia centralizzata. Dopo aver selezionato questa casella di controllo, sarà necessario configurare il VTherm che avrà il controllo della caldaia centralizzata. Se un VTherm richiede il riscaldamento, la caldaia verrà accesa. Se nessun VTherm richiede il riscaldamento, la caldaia verrà spenta. I comandi per accendere/spegnere la caldaia centrale sono riportati nella pagina di configurazione corrispondente.", + "use_auto_start_stop_feature": "Usa la funzione auto start and stop", + "use_heating_failure_detection_feature": "Usa rilevamento guasti riscaldamento" + } + }, + "type": { + "title": "Carateristiche - {name}", + "description": "Attributi delle entità collegate", + "data": { + "underlying_entity_ids": "Dispositivo(i) da controllare", + "heater_keep_alive": "intervallo di mantenimento della connessione in secondi", + "proportional_function": "Algoritmo", + "ac_mode": "Modo AC ?", + "sync_device_internal_temp": "Sincronizzare la temperatura interna del dispositivo", + "auto_regulation_mode": "Auto-regolazione", + "auto_regulation_dtemp": "Regulazione soglia", + "auto_regulation_periode_min": "Periodo minimo di regulazione", + "auto_regulation_use_device_temp": "Utilizzare la temperatura interna del sottostante", + "inverse_switch_command": "Invertire il comando di commutazione", + "auto_fan_mode": "Modo Auto fan", + "on_command_text": "Personalizzazione comando di accensione", + "vswitch_on_command": "Comando di accensione opzionale", + "off_command_text": "Personalizzazione comando di spegnimento", + "vswitch_off_command": "Comando di spegnimento opzionale" + }, + "data_description": { + "underlying_entity_ids": "Dispositivo(i) da controllare - è richiesto almeno 1", + "heater_keep_alive": "Intervallo di aggiornamento dello stato dell'interruttore del riscaldatore opzionale. Lasciare vuoto se non richiesto.", + "proportional_function": "Algoritmo da utilizzare (altri algoritmi possono essere installati come {plugins_link})", + "ac_mode": "Utilizzare la modalità Aria condizionata (AC)", + "sync_device_internal_temp": "Configurazione per sincronizzare la temperatura interna dei dispositivi sottostanti con le entità selezionate", + "auto_regulation_mode": "Correzione automatica della temperatura desiderata", + "auto_regulation_dtemp": "La soglia in ° (o % per la valvola) al di sotto della quale la variazione di temperatura non verrà inviata", + "auto_regulation_periode_min": "Durata in minuti tra due aggiornamenti della regolazione", + "auto_regulation_use_device_temp": "Utilizzare l'eventuale sensore di temperatura interno del sottostante per accelerare l'autoregolazione (non necessario nella maggior parte dei casi)", + "inverse_switch_command": "Per gli interruttori con filo pilota e diodo potrebbe essere necessario invertire il comando", + "auto_fan_mode": "Attivare automaticamente la ventilazione quando è necessario un riscaldamento/raffreddamento intenso", + "on_command_text": "Per i sottostanti di tipo “select” o “climate” è necessario personalizzare i comandi." + } + }, + "tpi": { + "title": "TPI - {name}", + "description": "Attributi dell'integrale proporzionale al tempo", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "tpi_threshold_low": "Soglia minima TPI", + "tpi_threshold_high": "Soglia massima TPI", + "minimal_activation_delay": "Ritardo minimo di attivazione", + "minimal_deactivation_delay": "Ritardo minimo di disattivazione", + "use_tpi_central_config": "Utilizzare la configuration TPI centralizzata", + "auto_tpi_mode": "Abilita Auto apprendimento TPI" + }, + "data_description": { + "tpi_coef_int": "Coefficiente da utilizzare per il delta della temperatura interna", + "tpi_coef_ext": "Coefficiente da utilizzare per il delta della temperatura esterna", + "tpi_threshold_low": "Soglia in ° al di sotto della quale l'algoritmo TPI sarà attivo. 0 significa nessuna soglia", + "tpi_threshold_high": "Soglia in ° al di sopra della quale l'algoritmo TPI sarà disattivo. 0 significa nessuna soglia", + "use_tpi_central_config": "Selezionare per utilizzare la configurazione TPI centralizzata. Deselezionare per utilizzare una configurazione TPI specifica per questo VTherm", + "minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata", + "minimal_deactivation_delay": "Ritardo in secondi durante il quale l'apparecchiatura rimarrà attiva", + "auto_tpi_mode": "Le sessioni di apprendimento TPI vengono avviate manualmente, leggere prima la documentazione." + } + }, + "presets": { + "title": "Preimpostazioni - {name}", + "description": "Selezionare se il termostato utilizza le impostazioni predefinite centralizzate. Deselezionare se il termostato ha le proprie entità predefinite", + "data": { + "use_presets_central_config": "Utilizzare preimpostazioni centralizzate" + } + }, + "window": { + "title": "Finestra - {name}", + "description": "Gestione finestra aperta.\nÈ possibile configurare il rilevamento automatico dell'apertura delle finestre in base alla diminuzione della temperatura", + "data": { + "window_sensor_entity_id": "Entity id del sensore finestra", + "window_delay": "Ritardo di attivazione sensore finestra (secondi)", + "window_off_delay": "Ritardo di disattivazione sensore finestra (secondi)", + "window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico dell'apertura della finestra (in °/ora)", + "window_auto_close_threshold": "Soglia di aumento della temperatura per il rilevamento automatico della chiusura della finestra (in °/ora)", + "window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min.)", + "use_window_central_config": "Uitilizzare Configurazione centralizzata della finestra", + "window_action": "Azione" + }, + "data_description": { + "window_sensor_entity_id": "Lasciare vuoto se non si desidera utilizzare alcun sensore finestra e si desidera utilizzare il rilevamento automatico", + "window_delay": "Rtardo in secondi prima che lo stato “on” del sensore venga preso in considerazione", + "window_off_delay": "Rtardo in secondi prima che lo stato “off\" del sensore venga preso in considerazione. Lasciare vuoto per utilizzare lo stesso valore della stato \"on\"", + "window_auto_open_threshold": "Valore consigliato: tra 3 e 10. Lasciare vuoto se non si utilizza il rilevamento automatico dell'apertura della finestra", + "window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se non si utilizza il rilevamento automatico dell'apertura della finestra", + "window_auto_max_duration": "Valore consigliato: 60 (1 ora). Lasciare vuoto se non si utilizza il rilevamento automatico dell'apertura della finestra", + "use_window_central_config": "Selezionare per utilizzare la configurazione avanzata centrale. Deselezionare per utilizzare una configurazione avanzata specifica per questo VTherm..", + "window_action": "Azione da eseguire se la finestra viene rilevata come aperta" + } + }, + "motion": { + "title": "Movimento - {name}", + "description": "Gestione del movimento. L'impostazione predefinita può cambiare automaticamente in base al rilevamento del movimento\nPreimpostazione movimento e Preimpostazione no-movimento devono essere impostati sul nome del preset corrispondente", + "data": { + "motion_sensor_entity_id": "Entity id del sensore di movimento", + "motion_delay": "Ritardo di attivazione", + "motion_off_delay": "Ritardo di disattivazione", + "motion_preset": "Preimpostazione movimento", + "no_motion_preset": "Preimpostazione no-movimento", + "use_motion_central_config": "Usare configurazione centralizzata del movimento" + }, + "data_description": { + "motion_sensor_entity_id": "L'entity id del sensore di movimento", + "motion_delay": "Ritardo di attivazione dovuto al movimento (secondi)", + "motion_off_delay": "Ritardo di disattivazione dovuto al mancato movimento (secondi)", + "motion_preset": "Preimpostazione da utilizzare quando viene rilevato un movimento", + "no_motion_preset": "Preimpostazione da utilizzare quando non viene più rilevato movimento", + "use_motion_central_config": "Selezionare per utilizzare la configurazione centralizzata del movimento. Deselezionare per utilizzare una configurazione specifica per questo VTherm" + } + }, + "power": { + "title": "Potenza- {name}", + "description": "Attributi di gestione dell'eneregia elettrica.\nIndica la potenza e il sensore della potenza massima della tua abitazione.\nSpecifica il consumo energetico del riscaldatore quando è acceso.\nTutti i sensori e l'alimentazione dei dispositivi devono utilizzare la stessa unità di misura (kW o W)", + "data": { + "power_sensor_entity_id": "Potenza", + "max_power_sensor_entity_id": "Potenza massima", + "power_temp": "Distribuzione Temperatura", + "use_power_central_config": "Utilizzare configurazione centralizzata della potenza" + }, + "data_description": { + "power_sensor_entity_id": "Entity id del sensore di potenza", + "max_power_sensor_entity_id": "Entity id del ensore della potenza massima", + "power_temp": "Temperature per la distribuzione della potenza", + "use_power_central_config": "Selezionare per utilizzare la configurazione centralizzata della potenza. Deselezionare per utilizzare una configurazione specifica per questo VTherm." + } + }, + "presence": { + "title": "Presenza - {name}", + "description": "Attributi di gestione della presenza.\nFornisce un sensore di presenza della tua casa (vero se qualcuno è presente) e fornisce l'impostazione predefinita della temperatura corrispondente", + "data": { + "presence_sensor_entity_id": "Sensore di presenza", + "use_presence_central_config": "Utilizza la configurazione centralizzata della temperatura in presenza. Deseleziona per utilizzare entità di temperatura specifiche" + }, + "data_description": { + "presence_sensor_entity_id": "Entity id del sensore di presenza" + } + }, + "advanced": { + "title": "Avanzate - {name}", + "description": "Configurazione dei parametri avanzati. Lascia i valori predefiniti se non sai cosa stai facendo.\nQuesti parametri possono causare un controllo della temperatura molto scarso o una cattiva regolazione della potenza", + "data": { + "minimal_activation_delay": "Ritardo minimo di attivazione", + "minimal_deactivation_delay": "Ritardo minimo di disattivazione", + "safety_delay_min": "Ritardo di sicurezza (in minuti)", + "safety_min_on_percent": "Percentuale minima di potenza per attivare la modalità di sicurezza", + "safety_default_on_percent": "Percentuale di potenza da utilizzare in modalità di sicurezza", + "repair_incorrect_state": "Ripara lo stato non corretto", + "use_advanced_central_config": "Utilizza la configurazione avanzata centrale" + }, + "data_description": { + "minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata", + "minimal_deactivation_delay": "Ritardo in secondi durante il quale l'apparecchiatura rimarrà attiva", + "safety_delay_min": "Ritardo massimo consentito in minuti tra due misurazioni della temperatura. Superato questo ritardo, il termostato passerà allo stato di spegnimento di sicurezza", + "safety_min_on_percent": "Valore minimo percentuale di riscaldamento per l'attivazione della preimpostazione di sicurezza. Al di sotto di tale percentuale di potenza, il termostato non entrerà nella preimpostazione di sicurezza", + "safety_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nell'impostazione di sicurezza. Impostare su 0 per spegnere il riscaldatore nell'impostazione di sicurezza", + "repair_incorrect_state": "Ripara automaticamente lo stato non corretto delle entità sottostanti quando rilevato. La funzione invia nuovamente il comando desiderato se lo stato del dispositivo sottostante non corrisponde allo stato previsto.", + "use_advanced_central_config": "Selezionare per utilizzare la configurazione avanzata centrale. Deselezionare per utilizzare una configurazione avanzata specifica per questo VTherm" + } + }, + "central_boiler": { + "title": "Controllo della caldaia centralizzata - {name}", + "description": "Inserire i servizi da chiamare per accendere/spegnere la caldaia centralizzata. Il servizio chiamato deve essere formattato come segue: `entity_id/nome_servizi[/attributo:valore]` (/attributo:valore è facoltativo)\nAd esempio:\n- per accendere un interruttore: `switch.controle_chaudiere/switch.turn_on`\n- per spegnere un interruttore: `switch.controle_chaudiere/switch.turn_off`\n- per programmare la caldaia a 25° e quindi forzarne l'accensione: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- per inviare 10° alla caldaia e quindi forzarne lo spegnimento: `climate. thermostat_chaudiere/climate.set_temperature/temperature:10`", + "data": { + "central_boiler_activation_service": "Comando per accendere", + "central_boiler_deactivation_service": "Comando per spegnere", + "central_boiler_activation_delay_sec": "Ritardo di attivazione (in secondi)", + "keep_alive_boiler_delay_sec": "Ritardo keep-alive (secondi)" + }, + "data_description": { + "central_boiler_activation_service": "Comando per accendere la caldaia centralizzata formattato come entity_id/nome_servizio[/attributo:valore]", + "central_boiler_deactivation_service": "Comando per spegnere la caldaia centralizzata formattato come entity_id/nome_servizio[/attributo:valore]", + "central_boiler_activation_delay_sec": "Ritardo in secondi prima di attivare la caldaia centralizzata dopo che un VTherm ha richiesto l'accensione", + "keep_alive_boiler_delay_sec": "Ritardo in secondi per inviare periodicamente il comando della caldaia se keep-alive è abilitato. Impostare su 0 per disabilitare keep-alive." + } + }, + "valve_regulation": { + "title": "Auto-regolazione con valvola - {name}", + "description": "Configurazione per l'autoregolazione con controllo diretto della valvola", + "data": { + "offset_calibration_entity_ids": "Entità di calibrazione offset", + "opening_degree_entity_ids": "Entità del grado di apertura", + "closing_degree_entity_ids": "Entità del grado di chiusura", + "proportional_function": "Algoritmo", + "min_opening_degrees": "Gradi di apertura minima", + "opening_threshold_degree": "Soglia di apertura", + "max_opening_degrees": "Grado di apertura massimo", + "max_closing_degree": "Grado di chiusura massimo" + }, + "data_description": { + "offset_calibration_entity_ids": "Elenco delle entità di “calibrazione dell'offset”. Impostalo se il tuo TRV dispone dell'entità per una migliore regolazione. Dovrebbe essercene una per ogni entità climatica sottostante", + "opening_degree_entity_ids": "Elenco delle entità “grado di apertura”. Dovrebbe essercene una per ciascuna entità climatica sottostante", + "closing_degree_entity_ids": "Elenco delle entità “grado di chiusura”. Impostarlo se il TRV dispone dell'entità per una migliore regolazione. Dovrebbe essercene uno per ogni entità climatica sottostante", + "proportional_function": "Algoritmo da utilizzare (altri algoritmi possono essere installati come {plugins_link})", + "min_opening_degrees": "Valore minimo del grado di apertura per ciascun dispositivo sottostante, separato da virgola. Il valore predefinito è 0. Esempio: 20, 25, 30", + "opening_threshold_degree": "Valore di apertura della valvola al di sotto del quale la valvola deve essere considerata chiusa (e quindi si applicherà 'max_closing_degree')", + "max_opening_degrees": "Valore massimo del grado di apertura per ogni dispositivo sottostante, separato da virgole. Il valore predefinito è 100. La valvola non si aprirà mai più di questo valore. Esempio: 80, 85, 90", + "max_closing_degree": "Valore massimo del grado di chiusura. La valvola non sarà mai chiusa più di questo valore. Impostare a 100 per chiudere completamente la valvola" + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - Configurazione - {name}", + "description": "Impostazioni generali per l'apprendimento automatico del TPI.", + "data": { + "auto_tpi_learning_type": "Tipo di apprendimento", + "auto_tpi_heating_rate": "Velocità di riscaldamento ({unit}/h)", + "heater_heating_time": "Tempo di riscaldamento (min)", + "heater_cooling_time": "Tempo di raffrescamento (min)", + "auto_tpi_aggressiveness": "Aggressività", + "auto_tpi_enable_advanced_settings": "Abilita parametri avanzati", + "auto_tpi_continuous_kext": "Apprendimento continuo di Kext" + }, + "data_description": { + "auto_tpi_learning_type": "Scegliere “Discovery” per l'avvio iniziale (media ponderata, peso 1) o “Fine Tuning” per le regolazioni continue (EWMA, Alpha 0,08).", + "auto_tpi_heating_rate": "Capacità di aumento della temperatura del radiatore ({unit} all'ora). Lasciare a 0 per l'apprendimento automatico (bootstrap).", + "heater_heating_time": "Tempo necessario per raggiungere la piena potenza (minuti)", + "heater_cooling_time": "Tempo di raffreddamento dopo l'arresto (minuti)\n\n| Tipo | Tempo di riscaldamento | Tempo di raffreddamento |\n| :--- | :--- | :--- |\n| Radiatore elettrico | 5 min | 7 min |\n| Radiatore ad acqua | 15 min | 20 min |\n| Riscaldamento a pavimento | 30 min | 45 min |", + "auto_tpi_aggressiveness": "Fattore di riduzione per i coefficienti appresi (50-100%). Valori inferiori determinano coefficienti più conservativi e riducono il rischio di sovrariscaldamento.", + "auto_tpi_enable_advanced_settings": "Controlla per modificare i parametri dell'algoritmo scelto.", + "auto_tpi_continuous_kext": "Selezionare per attivare la regolazione continua del coefficiente esterno (Kext) al di fuori della sessione AutoTPI." + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - Media ponderata - {name}", + "description": "Parametri per il metodo della media ponderata.", + "data": { + "auto_tpi_avg_initial_weight": "Peso iniziale" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Peso iniziale dei coefficienti esistenti nel calcolo della media (1-50 è il valore tipico). Un peso maggiore comporta un apprendimento più lento e più stabile." + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA - {name}", + "description": "Parametri per il metodo EWMA.\n\nRaccomandazioni:\n| Situazione | Alpha (ema_alpha) | Tasso di decadimento (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Regolazioni significative | 0,15 | 0,08 |\n| Regolazioni di precisione | 0,08 | 0,12 |", + "data": { + "auto_tpi_ema_alpha": "Alpha", + "auto_tpi_ema_decay_rate": "Tasso di decadimento", + "auto_tpi_continuous_kext_alpha": "Alfa Apprendimento continuo" + }, + "data_description": { + "auto_tpi_ema_alpha": "Fattore di livellamento (0-1). Più alto = adattamento più rapido", + "auto_tpi_ema_decay_rate": "Tasso di diminuzione di Alpha nel tempo (stabilizzazione)", + "auto_tpi_continuous_kext_alpha": "Fattore di livellamento per l'apprendimento continuo (Alpha). Impostazione predefinita 0,04 (adattamento circa 3-5 giorni)." + } + }, + "sync_device_internal_temp": { + "title": "Synchronize device internal temperature", + "description": "Configuration to synchronize the internal temperature of the underlying devices with selected entities", + "data": { + "sync_with_calibration": "Apply offset calibration", + "sync_entity_ids": "Entities id used for synchronization" + }, + "data_description": { + "sync_with_calibration": "Check to apply the offset calibration when synchronizing the internal temperature. Uncheck to directly copy the temperature from the selected entities", + "sync_entity_ids": "The list of the entities used to synchronize the internal temperature of the underlying devices. There should be one per underlying device. Should be a calibration entity if checkbox is selected or a temperature entity otherwise. Both are `number` entities." + } + }, + "heating_failure_detection": { + "title": "Rilevamento guasti riscaldamento - {name}", + "description": "Configura la funzione di rilevamento guasti riscaldamento. Rileva anomalie quando il riscaldamento è previsto ma la temperatura non aumenta, o quando il riscaldamento è spento ma la temperatura continua a salire.", + "data": { + "use_heating_failure_detection_feature": "Abilita rilevamento guasti riscaldamento", + "use_heating_failure_detection_central_config": "Usa configurazione centrale per rilevamento guasti", + "heating_failure_threshold": "Soglia guasto riscaldamento", + "cooling_failure_threshold": "Soglia guasto raffreddamento", + "heating_failure_detection_delay": "Ritardo rilevamento (minuti)", + "temperature_change_tolerance": "Tolleranza variazione temperatura (°C)", + "failure_detection_enable_template": "Template abilitazione rilevamento" + }, + "data_description": { + "use_heating_failure_detection_feature": "Abilita il rilevamento di anomalie di riscaldamento per VTherm con TPI", + "use_heating_failure_detection_central_config": "Seleziona per usare la configurazione centrale di rilevamento guasti. Deseleziona per usare parametri specifici per questo VTherm", + "heating_failure_threshold": "Soglia percentuale di accensione sopra la quale il riscaldamento dovrebbe causare un aumento di temperatura (0.9 = 90%)", + "cooling_failure_threshold": "Soglia percentuale di accensione sotto la quale la temperatura non dovrebbe aumentare (0.0 = 0%)", + "heating_failure_detection_delay": "Tempo in minuti da attendere prima di verificare se la temperatura è cambiata come previsto", + "temperature_change_tolerance": "Variazione minima di temperatura in gradi da considerare significativa. Variazioni minori vengono ignorate per filtrare il rumore del sensore (predefinito: 0.5°C)", + "failure_detection_enable_template": "Un template Jinja2 opzionale che deve restituire True per abilitare il rilevamento. Utile per disabilitare temporaneamente il rilevamento quando una fonte di calore esterna è attiva (es., `is_state('binary_sensor.stufa_legna', 'off')` tra doppie parentesi graffe). Se vuoto, il rilevamento è sempre abilitato." + } + } + }, + "error": { + "unknown": "Errore inatteso", + "unknown_entity": "entity id sconosciuto", + "window_open_detection_method": "È necessario utilizzare un solo metodo di rilevamento delle finestre aperte. Utilizzare il sensore della finestra o il rilevamento automatico tramite soglia di temperatura, ma non entrambi.", + "no_central_config": "Non è possibile selezionare “usa configurazione centralizzata” perché non è stata trovata alcuna configurazione centralizzata. È necessario creare un Versatile Thermostat di tipo “Configurazione centralizzata” per poterlo utilizzare.", + "service_configuration_format": "Il formato della configurazione del servizio è errato", + "valve_regulation_nb_entities_incorrect": "Il numero di entità della valvola per la regolazione delle valvole deve essere pari al numero dei sottostanti", + "sync_device_internal_temp_nb_entities_incorrect": "Il numero di entità per sincronizzare la temperatura interna del dispositivo dovrebbe essere uguale al numero dei sottostanti", + "min_opening_degrees_format": "È richiesto un elenco di numeri interi positivi separati da virgola. Esempio: 20, 25, 30", + "max_opening_degrees_format": "È previsto un elenco di numeri interi positivi tra 0 e 100 separati da virgola. Esempio: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Per ogni dispositivo sottostante, max_opening_degrees deve essere strettamente maggiore di min_opening_degrees. Verificare la configurazione.", + "vswitch_configuration_incorrect": "La configurazione della personalizzazione dei comandi non è corretta. È obbligatoria per i sottostanti non switch e il formato deve essere ‘nome_servizio[/attributo:valore]’. Maggiori informazioni nel file README." + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "selector": { + "thermostat_type": { + "options": { + "thermostat_central_config": "Configurazione centralizzata", + "thermostat_over_switch": "Termostato comanda una commutazione", + "thermostat_over_climate": "Termostato comanda un'entità clima", + "thermostat_over_valve": "Termostato comanda una valvola" + } + }, + "auto_regulation_mode": { + "options": { + "auto_regulation_slow": "Lento", + "auto_regulation_strong": "Forte", + "auto_regulation_medium": "Medio", + "auto_regulation_light": "Leggero", + "auto_regulation_expert": "Esperto", + "auto_regulation_none": "Nessuna auto-regolazione", + "auto_regulation_valve": "Controllo diretto di una valvola" + } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "Nessuna ventilazione automatica", + "auto_fan_low": "Basso", + "auto_fan_medium": "Medio", + "auto_fan_high": "Alto", + "auto_fan_turbo": "Turbo" + } + }, + "window_action": { + "options": { + "window_turn_off": "Comando per spegnere", + "window_fan_only": "Solo Ventilazione", + "window_frost_temp": "Antigelo", + "window_eco_temp": "Eco" + } + }, + "presets": { + "options": { + "frost": "Antigelo", + "eco": "Eco", + "comfort": "Comfort", + "boost": "Boost" + } + }, + "auto_start_stop": { + "options": { + "auto_start_stop_none": "Nessun avvio/arresto automatico", + "auto_start_stop_very_slow": "Rilevamento molto lento", + "auto_start_stop_slow": "Rilevamento lento", + "auto_start_stop_medium": "Rilevamento medio", + "auto_start_stop_fast": "Rilevamento veloce" + } + }, + "auto_tpi_calculation_method": { + "options": { + "average": "Media", + "ema": "Media mobile esponenziale (EMA)" + } + }, + "auto_tpi_learning_type": { + "options": { + "discovery": "Discovery", + "fine_tuning": "Fine Tuning" + } + } + }, + "entity": { + "climate": { + "versatile_thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "power": "Distribuzione", + "safety": "Sicurezza", + "none": "Manuale", + "frost": "Antigelo" + } + } + } + } + }, + "binary_sensor": { + "safety_state": { + "name": "Stato di sicurezza" + }, + "heating_failure_state": { + "name": "Stato di guasto del riscaldamento" + }, + "overpowering_state": { + "name": "Stato sovrapotenza" + }, + "window_state": { + "name": "Stato finestra" + }, + "motion_state": { + "name": "Stato movimento" + }, + "presence_state": { + "name": "Stato presenza" + }, + "window_bypass_state": { + "name": "Bypass finestra" + }, + "central_boiler_state": { + "name": "Caldaia centralizzata" + } + }, + "number": { + "frost_temp": { + "name": "Antigelo " + }, + "eco_temp": { + "name": "Eco" + }, + "comfort_temp": { + "name": "Comfort" + }, + "boost_temp": { + "name": "Boost" + }, + "frost_ac_temp": { + "name": "Antigelo ac" + }, + "eco_ac_temp": { + "name": "Eco ac" + }, + "comfort_ac_temp": { + "name": "Comfort ac" + }, + "boost_ac_temp": { + "name": "Boost ac" + }, + "frost_away_temp": { + "name": "Antigelo fuori casa" + }, + "eco_away_temp": { + "name": "Eco fuori casa" + }, + "comfort_away_temp": { + "name": "Comfort fuori casa" + }, + "boost_away_temp": { + "name": "Boost fuori casa" + }, + "eco_ac_away_temp": { + "name": "Eco ac fuori casa" + }, + "comfort_ac_away_temp": { + "name": "Comfort ac fuori casa" + }, + "boost_ac_away_temp": { + "name": "Boost ac fuori casa" + } + } + }, + "services": { + "set_auto_tpi_mode": { + "name": "Imposta modo Auto TPI", + "description": "Abilita o disabilita la modalità di apprendimento automatico TPI", + "fields": { + "auto_tpi_mode": { + "name": "Modo Auto TPI", + "description": "Abilita (vero) o disabilita (falso) l'apprendimento automatico del TPI" + }, + "reinitialise": { + "name": "Reset dati di apprendimento", + "description": "Reimposta tutti i dati di apprendimento quando si abilita la modalità Auto TPI (impostazione predefinita: true)" + } + } + }, + "reset_auto_tpi_capacities": { + "name": "Reimposta capacità TPI automatiche", + "description": "Reimpostare le capacità massime di riscaldamento e raffreddamento apprese a 1,0 °C/h. I coefficienti appresi NON vengono reimpostati." + }, + "auto_tpi_calibrate_capacity": { + "name": "Capacità di calibrazione automatica Tpi", + "description": "Calibrare la capacità di riscaldamento/raffreddamento (in °C/ora) utilizzando la regressione lineare sui dati storici dell'entità. I coefficienti appresi NON vengono aggiornati.", + "fields": { + "start_date": { + "name": "Storico dalla data", + "description": "Data da cui recuperare la cronologia (l'impostazione predefinita è 30 giorni fa)" + }, + "end_date": { + "name": "Storico alla data", + "description": "Data fino alla quale recuperare la cronologia (l'impostazione predefinita è ora)" + }, + "hvac_mode": { + "name": "Modo HVAC", + "description": "Quale modalità calibrare (riscaldamento o raffreddamento)." + }, + "save_to_config": { + "name": "Salva in Config", + "description": "Applicare la capacità calcolata alla configurazione Auto TPI." + }, + "min_power_threshold": { + "name": "Soglia minima di potenza", + "description": "Percentuale minima di potenza per considerare un ciclo come saturo (impostazione predefinita 95%). Valori inferiori includeranno più cicli, ma potrebbero essere meno accurati." + } + } + }, + "set_tpi_parameters": { + "name": "Imposta parametri TPI", + "description": "Modifica i parametri TPI", + "fields": { + "minimal_activation_delay": { + "name": "Ritardo minimo di attivazione", + "description": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata" + }, + "minimal_deactivation_delay": { + "name": "Ritardo minimo di disattivazione", + "description": "Ritardo in secondi durante il quale l'apparecchiatura rimarrà attiva" + }, + "tpi_threshold_low": { + "name": "Soglia bassa TPI", + "description": "Soglia in ° al di sotto della quale l'algoritmo TPI sarà attivo. 0 significa nessuna soglia" + }, + "tpi_threshold_high": { + "name": "Soglia alta TPI", + "description": "Soglia in ° al di sopra della quale l'algoritmo TPI sarà spento. 0 significa nessuna soglia" + } + } + }, + "lock": { + "name": "Blocco", + "description": "Blocca il termostato, impedendo qualsiasi modifica alla sua configurazione dall'interfaccia utente o dalle automazioni.", + "fields": { + "code": { + "name": "Codice di blocco", + "description": "Il codice di blocco opzionale" + } + } + }, + "unlock": { + "name": "Sblocca", + "description": "Sblocca il termostato, consentendo di modificarne la configurazione.", + "fields": { + "code": { + "name": "Codice di blocco", + "description": "Le code de verrouillage optionnel" + } + } + }, + "download_logs": { + "name": "Télécharger les logs", + "description": "Il codice di blocco opzionale.", + "fields": { + "log_level": { + "name": "Livello di log", + "description": "Livello di log minimo da includere" + }, + "period_start": { + "name": "Inizio periodo", + "description": "Inizio del periodo di estrazione" + }, + "period_end": { + "name": "Fine periodo", + "description": "Fine del periodo di estrazione" + } + } + } + }, + "exceptions": { + "auto_tpi_learning_stopped": { + "message": "L'apprendimento automatico TPI per {name} è stato interrotto a causa di 3 errori consecutivi. Motivo: {reason}. Controllare la configurazione." + } + } +} diff --git a/custom_components/versatile_thermostat/translations/pl.json b/custom_components/versatile_thermostat/translations/pl.json new file mode 100644 index 0000000..bd5fa29 --- /dev/null +++ b/custom_components/versatile_thermostat/translations/pl.json @@ -0,0 +1,940 @@ +{ + "title": "Termostat VTherm", + "config": { + "flow_title": "Konfiguracja termostatu VTherm", + "step": { + "user": { + "title": "Typ termostatu VTherm", + "description": "Wybierz typ termostatu, który chcesz utworzyć", + "data": { + "thermostat_type": "Typ termostatu" + }, + "data_description": { + "thermostat_type": "Dostępna jest wyłącznie konfiguracja centralna" + } + }, + "menu": { + "title": "Menu", + "description": "Skonfiguruj termostat. Konfiguracja zostanie zakończona po wprowadzeniu wszystkich wymaganych parametrów.", + "menu_options": { + "main": "Arybuty podstawowe", + "central_boiler": "Kocioł centralny", + "type": "Urządzenia", + "tpi": "Parametry TPI", + "features": "Funkcje", + "presets": "Preset", + "window": "Detekcja okna", + "motion": "Detekcja ruchu", + "power": "Zarządzanie energią", + "presence": "Detekcja obecności", + "advanced": "Parametry zaawansowane", + "auto_start_stop": "AutoSTART i autoSTOP", + "valve_regulation": "Konfiguracja regulacji zaworu", + "sync_device_internal_temp": "Synchronizacja temperatury urządzenia", + "lock": "Zarządzanie blokadą", + "finalize": "Wszystko zrobione", + "configuration_not_complete": "Konfiguracja niekompletna", + "heating_failure_detection": "Wykrywanie awarii ogrzewania" + } + }, + "lock": { + "title": "Zarządzanie blokadą", + "description": "Funkcja blokady uniemożliwia modyfikacje termostatu z poziomu interfejsu użytkownika lub automatyzacji, przy jednoczesnym zachowaniu działania termostatu.", + "data": { + "use_lock_central_config": "Użyj centralnej konfiguracji blokady", + "lock_code": "Opcjonalny kod PIN (4 cyfry)", + "lock_users": "Blokada uniemożliwi wykonywanie poleceń użytkowników", + "lock_automations": "Blokada uniemożliwi wykonywanie poleceń przez automatyzacje i integracje (np. harmonogram)", + "auto_relock_sec": "Automatyczne ponowne zablokowanie" + }, + "data_description": { + "use_lock_central_config": "Zaznacz, aby używać centralnej konfiguracji blokady. Odznacz, aby używać konfiguracji blokady dedykowanej dla tego termostatu VTherm", + "auto_relock_sec": "Opóźnienie w sekundach przed automatycznym ponownym zablokowaniem po odblokowaniu. Ustaw 0, aby wyłączyć. Domyślna wartość to 30 sekund." + } + }, + "main": { + "title": "Dodaj nowy termostat VTherm", + "description": "Wymagane atrybuty podstawowe", + "data": { + "name": "Nazwa", + "thermostat_type": "Typ termostatu", + "temperature_sensor_entity_id": "Temperatura pomieszczenia", + "last_seen_temperature_sensor_entity_id": "Czas ostatniego odczytu temperatury pomieszczenia", + "external_temperature_sensor_entity_id": "Identyfikator encji sensora temperatury zewnętrznej", + "cycle_min": "Czas trwania cyklu (w min.)", + "temp_min": "Minimalna temperatura dopuszczalna", + "temp_max": "Maksymalna temperatura dopuszczalna", + "step_temperature": "Krok temperatury", + "device_power": "Moc urządzenia", + "use_central_mode": "Załączenie sterowania przez encję centralną (wymaga konfiguracji centralnej). Zaznacz, aby załączyć sterowanie termostatu wybranymi encjami.", + "use_main_central_config": "Użyj dodatkowej konfiguracji centralnej. Zaznacz, aby korzystać z podstawowej konfiguracji centralnej (temperatura zewnętrzna, min, max, krok, itp.).", + "used_by_controls_central_boiler": "Używane przez kocioł centralny. Zaznacz, jeśli termostat ma sterować kotłem centralnym." + }, + "data_description": { + "cycle_min": "Minimalna długość jednego cyklu ogrzewania/chłodzenia w minutach, aby uniknąć zbyt częstego przełączania", + "temp_min": "Minimalna temperatura dopuszczalna dla termostatu", + "temp_max": "Maksymalna temperatura dopuszczalna dla termostatu", + "step_temperature": "Krok temperatury do ustawiania temperatury docelowej", + "temperature_sensor_entity_id": "Identyfikator encji sensora temperatury pomieszczenia", + "last_seen_temperature_sensor_entity_id": "Czas ostatniego odczytu temperatury pomieszczenia. Powinien być sensorem typu 'data/czas'", + "external_temperature_sensor_entity_id": "Identyfikator encji sensora temperatury zewnętrznej. Nie będzie używany, gdy wybrano konfigurację centralną" + } + }, + "features": { + "title": "Funkcje", + "description": "Funkcje termostatu", + "data": { + "use_window_feature": "Użyj detekcji okna", + "use_motion_feature": "Użyj detekcji ruchu", + "use_power_feature": "Użyj zarządzania energią", + "use_presence_feature": "Użyj detekcji obecności", + "use_central_boiler_feature": "Użyj kotła centralnego. Zaznacz, aby dodać sterowanie kotłem centralnym. Po zaznaczeniu tej opcji musisz skonfigurować termostat sterujący kotłem centralnym. Jeśli któryś z termostatów zażąda ogrzewania, kocioł się załączy. Jeśli żaden termostat nie będzie żądał ogrzewania, kocioł pozostanie wyłączony. Polecenia załączenia i wyłączenia kotła centralnego są podane na odpowiedniej stronie konfiguracyjnej", + "use_auto_start_stop_feature": "Użyj funkcji autoSTART i autoSTOP", + "use_heating_failure_detection_feature": "Użyj wykrywania awarii ogrzewania" + } + }, + "type": { + "title": "Encje powiązane", + "description": "Atrybuty encji powiązanych", + "data": { + "underlying_entity_ids": "Sterowane urządzenia", + "heater_keep_alive": "Czas podtrzymania (w sek.)", + "proportional_function": "Algorytm", + "ac_mode": "Tryb AC", + "sync_device_internal_temp": "Synchronizacja wewnętrznej temperatury urządzenia", + "auto_regulation_mode": "Autoregulacja", + "auto_regulation_dtemp": "Próg regulacji", + "auto_regulation_periode_min": "Minimalny okres regulacji", + "auto_regulation_use_device_temp": "Użyj temperatury wewnętrznej urządzenia", + "inverse_switch_command": "Polecenie odwrócenia przełącznika", + "auto_fan_mode": "Tryb autowentylacji", + "on_command_text": "Dostosowanie polecenia załączenia", + "vswitch_on_command": "Opcjonalne polecenia załączenia", + "off_command_text": "Dostosowanie polecenia wyłączenia", + "vswitch_off_command": "Opcjonalne polecenia wyłączenia" + }, + "data_description": { + "underlying_entity_ids": "Sterowane urządzenia. Wybierz co najmniej jedno urządzenie", + "heater_keep_alive": "Opcjonalna zwłoka odświeżania stanu przełącznika ogrzewania. Jeśli nie jest wymagana, pozostaw puste.", + "proportional_function": "Algorytm do użycia (inne algorytmy można zainstalować jako {plugins_link})", + "ac_mode": "Użyj trybu AC", + "sync_device_internal_temp": "Konfiguracja do synchronizacji wewnętrznej temperatury urządzeń bazowych z wybranymi encjami", + "auto_regulation_mode": "Automatyczna regulacja temperatury docelowej", + "auto_regulation_dtemp": "Próg otwarcia w ° (dla zaworu w %), poniżej którego zmiana temperatury nie będzie uwzględniana", + "auto_regulation_periode_min": "Czas (w min.) między dwiema aktualizacjami regulacji", + "auto_regulation_use_device_temp": "Użyj ewentualnego wewnętrznego czujnika temperatury urządzenia, aby przyspieszyć autoregulację (w większości przypadków nie jest to konieczne)", + "inverse_switch_command": "W przypadku przełącznika z przewodem sterującym może być konieczne odwrócenie kolejności poleceń", + "auto_fan_mode": "Automatycznie uruchom wentylator, gdy konieczne jest intensywne ogrzewanie/chłodzenie", + "on_command_text": "W przypadku typu `select` lub `climate` należy dostosować polecenia sterujące." + } + }, + "tpi": { + "title": "TPI", + "description": "Atrybuty TPI", + "data": { + "tpi_coef_int": "Delta temperatury wewnętrznej", + "tpi_coef_ext": "Delta temperatury zewnętrznej", + "tpi_threshold_low": "Niski próg TPI", + "tpi_threshold_high": "Wysoki próg TPI", + "use_tpi_central_config": "Użyj centralnej konfiguracji TPI", + "minimal_activation_delay": "Minimalna zwłoka czasowa aktywacji", + "minimal_deactivation_delay": "Minimalna zwłoka czasowa deaktywacji", + "auto_tpi_mode": "Aktywuj naukę Auto TPI" + }, + "data_description": { + "tpi_coef_int": "Współczynnik do zastosowania w przypadku delty temperatury wewnętrznej", + "tpi_coef_ext": "Współczynnik do zastosowania w przypadku delty temperatury zewnętrznej", + "tpi_threshold_low": "Próg w °, poniżej którego algorytm TPI będzie aktywny. 0 oznacza brak progu.", + "tpi_threshold_high": "Próg w °, powyżej którego algorytm TPI będzie nieaktywny. 0 oznacza brak progu.", + "use_tpi_central_config": "Zaznacz, aby używać centralnej konfiguracji TPI. Odznacz, aby używać konfiguracji TPI dedykowanej wyłącznie dla tego termostatu", + "minimal_activation_delay": "Zwłoka (w sek.), w czasie której urządzenie nie zostanie aktywowane", + "minimal_deactivation_delay": "Zwłoka (w sek.), w czasie której urządzenie będzie utrzymywane w stanie gotowości", + "auto_tpi_mode": "Sesje nauki TPI są uruchamiane ręcznie, najpierw przeczytaj dokumentację." + } + }, + "presets": { + "title": "Presety", + "description": "Wybierz, jeśli termostat ma używać presetów podstawowych. Wyłącz, jeśli termostat ma używać swoich własnych presetów", + "data": { + "use_presets_central_config": "Użyj konfiguracji presetów podstawowych" + } + }, + "window": { + "title": "Okna", + "description": "Zarządzanie otwarciem okna.\nMożesz także skonfigurować automatyczną detekcję otwarcia okna w oparciu o spadek temperatury", + "data": { + "window_sensor_entity_id": "Identyfikator encji sensora otwarcia okna", + "window_delay": "Zwłoka otwarcia okna (w sek.)", + "window_off_delay": "Zwłoka zamknięcia okna (w sek.)", + "window_auto_open_threshold": "Próg spadku temperatury dla automatycznej detekcji otwarcia okna (w °/godz.)", + "window_auto_close_threshold": "Próg wzrostu temperatury dla automatycznej detekcji zamknięcia okna (w °/godz.)", + "window_auto_max_duration": "Maksymalny czas trwania detekcji otwarcia okna (w min)", + "use_window_central_config": "Użyj centralnej konfiguracji okna", + "window_action": "Akcja" + }, + "data_description": { + "window_sensor_entity_id": "Pozostaw puste, jeśli nie chcesz używać sensora okna i chcesz użyć automatycznej detekcji", + "window_delay": "Zwłoka (w sek.) przed detekcją otwarcia okna", + "window_off_delay": "Zwłoka (w sek.) przed detekcją zamknięcia okna. Pozostaw puste, aby użyć tej samej wartości, jak dla detekcji otwarcia okna", + "window_auto_open_threshold": "Zalecana wartość: między 3 a 10. Pozostaw puste, jeśli automatyczna detekcja otwarcia okna nie jest używana.", + "window_auto_close_threshold": "Zalecana wartość: 0. Pozostaw puste, jeśli automatyczna detekcja otwarcia okna nie jest używana", + "window_auto_max_duration": "Zalecana wartość: 60 (w min.). Pozostaw puste, jeśli automatyczna detekcja otwarcia okna nie jest używana", + "use_window_central_config": "Wybierz, aby użyć centralnej konfiguracji okna. Wyłącz wybór, aby użyć konfiguracji okna dedykowanej dla tego termostatu", + "window_action": "Akcja do wykonania, gdy wykryte zostanie otwarcie okna" + } + }, + "motion": { + "title": "Ruch", + "description": "Zarządzanie sensorem ruchu. Presety mogą przełączać się automatycznie w zależności od wykrycia ruchu. Parametry `Preset ruchu` i `Preset braku ruchu` powinny być ustawione jako odpowiednie presety. ", + "data": { + "motion_sensor_entity_id": "Identyfikator encji sensora ruchu", + "motion_delay": "Zwłoka aktywacji", + "motion_off_delay": "Zwłoka deaktywacji", + "motion_preset": "Preset ruchu", + "no_motion_preset": "Preset braku ruchu", + "use_motion_central_config": "Użyj centralnej konfiguracji ruchu" + }, + "data_description": { + "motion_sensor_entity_id": "Identyfikator encji sensora ruchu", + "motion_delay": "Zwłoka aktywacji (w sek.)", + "motion_off_delay": "Zwłoka deaktywacji (w sek.)", + "motion_preset": "Preset, gdy wykryto ruch", + "no_motion_preset": "Preset, gdy brak wykrycia ruchu", + "use_motion_central_config": "Wybierz, aby użyć centralnej konfiguracji ruchu. Wyłącz wybór, aby użyć konfiguracji ruchu dedykowanej dla tego termostatu" + } + }, + "power": { + "title": "Energia", + "description": "Atrybuty zarządzania energią.\nWskazują moc i maksymalną moc sensora.\nOkreślają pobór mocy grzejnika, gdy jest włączony.\nWszystkie sensory i urządzenia powinny mieć tę samą jednostkę mocy (kW lub W).", + "data": { + "power_sensor_entity_id": "Moc", + "max_power_sensor_entity_id": "Moc maksymalna", + "power_temp": "Temperatura progowa redukująca zużycie energii", + "use_power_central_config": "Użyj centralnej konfiguracji energii" + }, + "data_description": { + "power_sensor_entity_id": "Identyfikator encji sensora energii", + "max_power_sensor_entity_id": "Identyfikator encji sensora energii maksymalnej", + "power_temp": "Temperatura progowa redukująca zużycie energii", + "use_power_central_config": "Wybierz, aby użyć centralnej konfiguracji energii. Wyłącz wybór, aby użyć konfiguracji energii dedykowanej dla tego termostatu" + } + }, + "presence": { + "title": "Obecność", + "description": "Atrybuty zarządzania obecnością.\nWskazują stan sensora obecności (PRAWDA, gdy wykrywana jest czyjaś obecność) oraz odpowiednie presety temperatury", + "data": { + "presence_sensor_entity_id": "Sensor obecności", + "use_presence_central_config": "Wybierz, aby użyć centralnej konfiguracji temperatury dla wykrytej obecności. Wyłącz wybór, aby użyć wybranych encji temperatury" + }, + "data_description": { + "presence_sensor_entity_id": "Identyfikator encji sensora obecności" + } + }, + "advanced": { + "title": "Parametry zaawansowane", + "description": "Konfiguracja parametrów zaawansowanych. Pozostaw wartości domyślne, jeśli nie wiesz, co zrobić.\nNieprawidłowa zmiana tych parametrów może prowadzić do błędnej kontroli temperatur lub błędnej regulacji mocy", + "data": { + "safety_delay_min": "Zwłoka trybu bezpiecznego (w min.)", + "safety_min_on_percent": "Minimalny procent mocy do przejścia w tryb bezpieczny", + "safety_default_on_percent": "Procent mocy do użycia trybu bezpiecznego", + "repair_incorrect_state": "Napraw nieprawidłowy stan", + "use_advanced_central_config": "Użyj zaawansowanej konfiguracji centralnej" + }, + "data_description": { + "safety_delay_min": "Maksymalna dopuszczalna zwłoka (w min.) między dwoma pomiarami temperatury. Powyżej tej zwłoki termostat przełączy się w stan wyłączenia trybu bezpiecznego.", + "safety_min_on_percent": "Minimalna wartość procentowa ogrzewania dla załączenia trybu bezpiecznego. Poniżej tej wartości termostat nie załączy trybu bezpiecznego.", + "safety_default_on_percent": "Domyślna wartość procentowa mocy grzewczej w trybie bezpiecznym. Ustaw na 0, aby wyłączyć ogrzewanie w bezpiecznym presecie", + "repair_incorrect_state": "Automatycznie napraw nieprawidłowy stan jednostek bazowych po wykryciu. Funkcja ponownie wyśle żądane polecenie, jeśli stan urządzenia bazowego nie odpowiada oczekiwanemu stanowi.", + "use_advanced_central_config": "Wybierz, aby użyć zaawansowanej konfiguracji centralnej. Wyłącz wybór, aby użyć konfiguracji zaawansowanej dedykowanej dla tego termostatu" + } + }, + "central_boiler": { + "title": "Sterowanie kotłem centralnym", + "description": "Wprowadź polecenia załączające i wyłączające kocioł centralny. Polecenia muszą mieć następujący format: `id_encji/nazwa_polecenia[/atrybut:wartość]` (/atrybut:wartość jest opcjonalny)\nNa przykład:\n- aby załączyć przełącznik: `switch.sterownik_pieca/switch.turn_on`\n- aby wyłączyć przełącznik: `switch.sterownik_pieca/switch.turn_off`\n- aby zaprogramować temperaturę kotła na 25° i tym samym wymusić jego zapłon: `climate.termostat_pieca/climate.set_temperature/temperature:25`\n- aby ustawić temperaturę kotła na 10° i tym samym wymusić jego wygaszenie: `climate.termostat_pieca/climate.set_temperature/temperature:10`", + "data": { + "central_boiler_activation_service": "Polecenie załączenia", + "central_boiler_deactivation_service": "Polecenie wyłączenia", + "central_boiler_activation_delay_sec": "Opóźnienie załączenia (w sekundach)", + "keep_alive_boiler_delay_sec": "Opóźnienie keep-alive (sekundy)" + }, + "data_description": { + "central_boiler_activation_service": "Polecenie załączenia kotła centralnego w formacie: id_encji/nazwa_polecenia[/atrybut:wartość]", + "central_boiler_deactivation_service": "Polecenie wyłączenia kotła centralnego w formacie: id_encji/nazwa_polecenia[/atrybut:wartość]", + "central_boiler_activation_delay_sec": "Opóźnienie (w sekundach) przed załączeniem kotła centralnego po tym, jak VTherm zażądał włączenia", + "keep_alive_boiler_delay_sec": "Opóźnienie w sekundach do okresowego ponownego wysłania polecenia kotła, jeśli keep-alive jest włączony. Ustaw na 0, aby wyłączyć keep-alive." + } + }, + "valve_regulation": { + "title": "Autoregulacja zaworu", + "description": "Konfiguracja autoregulacji z bezpośrednim sterowaniem zaworem", + "data": { + "offset_calibration_entity_ids": "Encje kalibracji", + "opening_degree_entity_ids": "Encje stopnia otwarcia", + "closing_degree_entity_ids": "Encje stopnia zamknięcia", + "proportional_function": "Algorytm", + "opening_threshold_degree": "Próg otwarcia zaworu", + "min_opening_degrees": "Minimalny stopień otwarcia", + "max_opening_degrees": "Maksymalny stopień otwarcia", + "max_closing_degree": "Maksymalny stopień zamknięcia" + }, + "data_description": { + "offset_calibration_entity_ids": "Lista encji kalibracji. Ustaw, jeśli zawór wyposażony jest w encję korekty regulacji. Dla każdego termostatu powinna istnieć maksymalnie jedna taka encja", + "opening_degree_entity_ids": "Lista encji 'stopnia otwarcia'. Powinna istnieć jedna dla każdego termostatu głównego.", + "closing_degree_entity_ids": "Lista encji 'stopnia zamknięcia'. Ustaw, jeśli zawór wyposażony jest w encję korekty regulacji. Dla każdego termostatu powinna istnieć co najwyżej jedna taka encja", + "proportional_function": "Algorytm do użycia (inne algorytmy można zainstalować jako {plugins_link})", + "opening_threshold_degree": "Próg otwarcia zaworu, poniżej którego zawór powinien być uważany za zamknięty (wówczas będzie obowiązywał parametr maksymalnego stopnia zamknięcia)", + "min_opening_degrees": "Minimalne wartości stopnia otwarcia zaworu dla każdego urządzenia, oddzielone przecinkiem. Domyślnie: 0. Przykład: 20, 25, 30", + "max_opening_degrees": "Maksymalna wartość stopnia otwarcia zaworu. Zawór nigdy się nie otworzy więcej niż ta wartość", + "max_closing_degree": "Maksymalna wartość stopnia zamknięcia zaworu. Powyżej tej wartości zawór nigdy nie zostanie zamknięty. Ustaw wartość tego parametru na `100`, aby całkowicie zamknąć zawór, jeśli ogrzewanie jest już niepotrzebne" + } + }, + "sync_device_internal_temp": { + "title": "Synchronizuj wewnętrzną temperaturę urządzenia", + "description": "Konfiguracja umożliwiająca synchronizację temperatury wewnętrznej urządzeń bazowych z wybranymi encjami", + "data": { + "sync_with_calibration": "Zastosuj kalibrację offsetu", + "sync_entity_ids": "Identyfikatory encji używanych w synchronizacji" + }, + "data_description": { + "sync_with_calibration": "Zaznacz, aby zastosować kalibrację offsetu podczas synchronizacji temperatury wewnętrznej. Odznacz, aby skopiować temperaturę bezpośrednio z wybranych encji.", + "sync_entity_ids": "Lista encji używanych do synchronizacji temperatury wewnętrznej urządzeń bazowych. Każde urządzenie bazowe musi mieć jedną taką encję. Jeśli pole wyboru jest zaznaczone, powinna to być encja kalibracji. W przeciwnym razie powinna to być encja temperatury. Obie encje mają typ numeryczny `number`." + } + }, + "heating_failure_detection": { + "title": "Wykrywanie awarii ogrzewania", + "description": "Konfiguracja funkcji wykrywania awarii ogrzewania. Wykrywa anomalie, gdy ogrzewanie jest oczekiwane, ale temperatura nie wzrasta, lub gdy ogrzewanie jest wyłączone, ale temperatura nadal rośnie.", + "data": { + "use_heating_failure_detection_feature": "Włącz wykrywanie awarii ogrzewania", + "use_heating_failure_detection_central_config": "Użyj centralnej konfiguracji wykrywania awarii", + "heating_failure_threshold": "Próg awarii ogrzewania", + "cooling_failure_threshold": "Próg awarii chłodzenia", + "heating_failure_detection_delay": "Opóźnienie wykrywania (minuty)", + "temperature_change_tolerance": "Tolerancja zmiany temperatury (°C)", + "failure_detection_enable_template": "Szablon włączenia wykrywania" + }, + "data_description": { + "use_heating_failure_detection_feature": "Włącz wykrywanie anomalii ogrzewania dla VTherm z TPI", + "use_heating_failure_detection_central_config": "Zaznacz, aby użyć centralnej konfiguracji wykrywania awarii. Odznacz, aby użyć specyficznych parametrów dla tego VTherm", + "heating_failure_threshold": "Próg procentowy włączenia, powyżej którego ogrzewanie powinno powodować wzrost temperatury (0.9 = 90%)", + "cooling_failure_threshold": "Próg procentowy włączenia, poniżej którego temperatura nie powinna wzrastać (0.0 = 0%)", + "heating_failure_detection_delay": "Czas w minutach do odczekania przed sprawdzeniem, czy temperatura zmieniła się zgodnie z oczekiwaniami", + "temperature_change_tolerance": "Minimalna zmiana temperatury w stopniach uznawana za znaczącą. Mniejsze zmiany są ignorowane w celu filtrowania szumu czujnika (domyślnie: 0.5°C)", + "failure_detection_enable_template": "Opcjonalny szablon Jinja2, który musi zwrócić True, aby włączyć wykrywanie. Przydatne do tymczasowego wyłączenia wykrywania, gdy zewnętrzne źródło ciepła jest aktywne (np. `is_state('binary_sensor.piec_na_drewno', 'off')` w podwójnych nawiasach klamrowych). Jeśli pusty, wykrywanie jest zawsze włączone." + } + } + }, + "error": { + "unknown": "Nieoczekiwany błąd", + "unknown_entity": "Nieznany identyfikator encji", + "window_open_detection_method": "Może być używana tylko jedna metoda detekcji otwarcia okna. Użyj sensora okna lub automatycznej detekcji przekroczenia progu temperatury, ale nie używaj obu tych metod jednocześnie.", + "no_central_config": "Nie możesz wybrać konfiguracji centralnej, ponieważ jej nie znaleziono. Aby jej użyć, najpierw musisz utworzyć Termostat VTherm typu `Konfiguracja Centralna`.", + "service_configuration_format": "Błędny format konfiguracji polecenia lub usługi", + "valve_regulation_nb_entities_incorrect": "Liczba encji zaworu wymagana do jego regulacji powinna być równa ilości zaworów", + "sync_device_internal_temp_nb_entities_incorrect": "The number of entities for synchronize device internal temperature should be equal to the number of underlyings", + "min_opening_degrees_format": "Oczekiwany separator przecinkowy listy lub dodatnia liczba całkowita. Przykład: 20, 25, 30", + "max_opening_degrees_format": "Oczekiwana jest lista dodatnich liczb całkowitych między 0 a 100 oddzielonych przecinkami. Przykład: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Dla każdego urządzenia bazowego max_opening_degrees musi być ściśle większy niż min_opening_degrees. Sprawdź swoją konfigurację.", + "vswitch_configuration_incorrect": "Błąd konfiguracji poleceń własnych. Wymagane są one dla urządzeń nie będących przełącznikami. Ich format musi być następujący: `service_name[/attribute:value]`. Więcej informacji znajdziesz w dokumentacji" + }, + "abort": { + "already_configured": "Urządzenie jest już skonfigurowane" + } + }, + "options": { + "flow_title": "Konfiguracja termostatu VTherm", + "step": { + "user": { + "title": "Typ - {name}", + "description": "Wybierz typ termostatu, który chcesz utworzyć", + "data": { + "thermostat_type": "Typ termostatu" + }, + "data_description": { + "thermostat_type": "Możliwy jest tylko jeden typ konfiguracji centralnej" + } + }, + "menu": { + "title": "Menu", + "description": "Skonfiguruj termostat. Dokończysz konfigurację, gdy wszystkie wymagane parametry zostaną wprowadzone.", + "menu_options": { + "main": "Arybuty podstawowe", + "central_boiler": "Kocioł centralny", + "type": "Urządzenia", + "tpi": "Parametry TPI", + "features": "Funkcje", + "presets": "Presety", + "window": "Detekcja okna", + "motion": "Detekcja ruchu", + "power": "Zarządzanie energią", + "presence": "Detekcja obecności", + "advanced": "Parametry zaawansowane", + "auto_start_stop": "AutoSTART i autoSTOP", + "valve_regulation": "Konfiguracja regulacji zaworu", + "sync_device_internal_temp": "Synchronizacja temperatury urządzenia", + "lock": "Blokada", + "auto_tpi_configuration": "Auto TPI - Ogólne", + "auto_tpi_avg_settings": "Auto TPI - Średnia", + "auto_tpi_ema_settings": "Auto TPI - EMA", + "finalize": "Wszystko zrobione", + "configuration_not_complete": "Konfiguracja niekompletna", + "heating_failure_detection": "Wykrywanie awarii ogrzewania" + } + }, + "lock": { + "title": "Zarządzanie blokadą - {name}", + "description": "Funkcja blokady zapobiega modyfikacjom termostatu z interfejsu użytkownika lub automatyzacji, jednocześnie utrzymując termostat w stanie pracy.", + "data": { + "use_lock_central_config": "Użyj centralnej konfiguracji blokady", + "lock_code": "Opcjonalny kod PIN (4 cyfry)", + "lock_users": "Blokada uniemożliwi polecenia użytkowników", + "lock_automations": "Blokada uniemożliwi polecenia z automatyzacji i integracji (np. harmonogram)", + "auto_relock_sec": "Automatyczne ponowne zablokowanie" + }, + "data_description": { + "use_lock_central_config": "Zaznacz aby używać centralnej konfiguracji blokady. Odznacz aby używać konfiguracji blokady specyficznej dla tego VTherm", + "auto_relock_sec": "Opóźnienie w sekundach przed automatycznym ponownym zablokowaniem po odblokowaniu. Ustaw 0, aby wyłączyć. Domyślna wartość to 30 sekund." + } + }, + "main": { + "title": "Główne - {name}", + "description": "Wymagane atrybuty podstawowe", + "data": { + "name": "Nazwa", + "thermostat_type": "Typ termostatu", + "temperature_sensor_entity_id": "Temperatura pomieszczenia", + "last_seen_temperature_sensor_entity_id": "Czas ostatniego odczytu temperatury pomieszczenia", + "external_temperature_sensor_entity_id": "Identyfikator encji sensora temperatury zewnętrznej", + "cycle_min": "Czas trwania cyklu (w min.)", + "temp_min": "Minimalna dopuszczalna temperatura", + "temp_max": "Maksymalna dopuszczalna temperatura", + "step_temperature": "Krok temperatury", + "device_power": "Moc urządzenia", + "use_central_mode": "Załączenie sterowania przez encję główną (wymaga konfiguracji centralnej). Zaznacz, aby załączyć sterowanie termostatu wybranymi encjami.", + "use_main_central_config": "Użyj dodatkowej konfiguracji centralnej. Zaznacz, aby używać konfiguracji centralnej (temperatura zewnętrzna, min, max, krok, itp.).", + "used_by_controls_central_boiler": "Używane przez kocioł centralny. Zaznacz, jeśli ten termostat powinien sterować kotłem centralnym" + }, + "data_description": { + "cycle_min": "Minimalna długość jednego cyklu ogrzewania/chłodzenia w minutach, aby uniknąć zbyt częstego przełączania", + "temp_min": "Minimalna temperatura dopuszczalna dla termostatu", + "temp_max": "Maksymalna temperatura dopuszczalna dla termostatu", + "step_temperature": "Krok temperatury do ustawiania temperatury docelowej", + "temperature_sensor_entity_id": "Identyfikator encji sensora temperatury pomieszczenia", + "last_seen_temperature_sensor_entity_id": "Czas ostatniego odczytu temperatury pomieszczenia. Powinien być sensorem typu 'data/czas'", + "external_temperature_sensor_entity_id": "Identyfikator encji sensora temperatury zewnętrznej. Nie będzie używany, gdy wybrano konfigurację centalną" + } + }, + "features": { + "title": "Funkcje - {name}", + "description": "Funkcje termostatu", + "data": { + "use_window_feature": "Użyj detekcji okna", + "use_motion_feature": "Użyj detekcji ruchu", + "use_power_feature": "Użyj zarządzania energią", + "use_presence_feature": "Użyj detekcji obecności", + "use_central_boiler_feature": "Użyj kotła centralnego. Zaznacz, aby dodać sterowanie kotłem centralnym. Po zaznaczeniu tej opcji musisz skonfigurować termostat sterujący kotłem centralnym. Jeśli któryś z termostatów będzie wymagał ogrzewania, kocioł się załączy. Jeśli żaden termostat nie będzie wymagał ogrzewania, kocioł pozostanie wyłączony. Polecenia załączenia i wyłączenia kotła centralnego są podane na odpowiedniej stronie konfiguracyjnej", + "use_auto_start_stop_feature": "Użyj funkcji autoSTART i autoSTOP", + "use_heating_failure_detection_feature": "Użyj wykrywania awarii ogrzewania" + } + }, + "type": { + "title": "Encje - {name}", + "description": "Atrybuty encji powiązanych", + "data": { + "underlying_entity_ids": "Sterowane urządzenie(-a)", + "heater_keep_alive": "Czas podtrzymania (w sek.)", + "proportional_function": "Algorytm", + "ac_mode": "Tryb AC", + "sync_device_internal_temp": "Synchronizacja wewnętrznej temperatury urządzenia", + "auto_regulation_mode": "Autoregulacja", + "auto_regulation_dtemp": "Próg regulacji", + "auto_regulation_periode_min": "Minimalny okres regulacji", + "auto_regulation_use_device_temp": "Użyj temperatury wewnętrznej z urządzenia", + "inverse_switch_command": "Polecenie odwrócenia przełącznika", + "auto_fan_mode": "Tryb autowentylacji", + "on_command_text": "Dostosowanie polecenia załączenia", + "vswitch_on_command": "Opcjonalne polecenia załączenia", + "off_command_text": "Dostosowanie polecenia wyłączenia", + "vswitch_off_command": "Opcjonalne polecenia wyłączenia" + }, + "data_description": { + "underlying_entity_ids": "Sterowane urządzenia. Wybierz co najmniej jedno urządzenie", + "heater_keep_alive": "Opcjonalny interwał odświeżania stanu przełącznika ogrzewania. Jeśli nie jest wymagany, pozostał pusty.", + "proportional_function": "Algorytm do użycia (inne algorytmy można zainstalować jako {plugins_link})", + "ac_mode": "Użyj trybu AC", + "sync_device_internal_temp": "Konfiguracja do synchronizacji wewnętrznej temperatury urządzeń bazowych z wybranymi encjami", + "auto_regulation_mode": "Automatyczna regulacja temperatury docelowej", + "auto_regulation_dtemp": "Próg otwarcia w ° (dla zaworu w %), poniżej którego zmiana temperatury nie będzie uwzględniana", + "auto_regulation_periode_min": "Czas (w min.) między dwiema aktualizacjami regulacji", + "auto_regulation_use_device_temp": "Użyj ewentualnego wewnętrznego sensora temperatury urządzenia, aby przyspieszyć autoregulację (w większości przypadków nie jest to konieczne)", + "inverse_switch_command": "W przypadku przełącznika z przewodem sterującym może być konieczna odwrotna kolejność polecenia", + "auto_fan_mode": "Automatycznie aktywuj wentylator, gdy konieczne jest intensywne ogrzewanie/chłodzenie", + "on_command_text": "W przypadku typu `select` lub `climate` należy dostosować polecenia." + } + }, + "tpi": { + "title": "TPI - {name}", + "description": "Atrybuty TPI", + "data": { + "tpi_coef_int": "Delta temperatury wewnętrznej", + "tpi_coef_ext": "Delta temperatury zewnętrznej", + "tpi_threshold_low": "Niski próg TPI", + "tpi_threshold_high": "Wysoki próg TPI", + "use_tpi_central_config": "Użyj centralnej konfiguracji TPI", + "minimal_activation_delay": "Minimalna zwłoka czasowa aktywacji", + "minimal_deactivation_delay": "Minimalna zwłoka czasowa deaktywacji", + "auto_tpi_mode": "Aktywuj naukę Auto TPI" + }, + "data_description": { + "tpi_coef_int": "Współczynnik do zastosowania w przypadku delty temperatury wewnętrznej", + "tpi_coef_ext": "Współczynnik do zastosowania w przypadku delty temperatury zewnętrznej", + "tpi_threshold_low": "Próg w °, poniżej którego algorytm TPI będzie aktywny. 0 oznacza brak progu.", + "tpi_threshold_high": "Próg w °, powyżej którego algorytm TPI będzie nieaktywny. 0 oznacza brak progu.", + "use_tpi_central_config": "Zaznacz, aby używać centralnej konfiguracji TPI. Odznacz, aby używać konfiguracji TPI dedykowanej wyłącznie dla tego termostatu", + "minimal_activation_delay": "Zwłoka (w sek.), w czasie której urządzenie nie zostanie aktywowane", + "minimal_deactivation_delay": "Zwłoka (w sek.), w czasie której urządzenie będzie utrzymywane w stanie gotowości", + "auto_tpi_mode": "Sesje nauki TPI są uruchamiane ręcznie, najpierw przeczytaj dokumentację." + } + }, + "presets": { + "title": "Presety - {name}", + "description": "Wybierz, jeśli termostat ma używać presetów podstawowych - wyłącz wybór, jeśli termostat ma używać swoich własnych presetów", + "data": { + "use_presets_central_config": "Użyj centralnej konfiguracji presetów" + } + }, + "window": { + "title": "Okno - {name}", + "description": "Zarządzanie otwarciem okna.\nMożesz także skonfigurować automatyczną detekcję otwarcia okna w oparciu o spadek temperatury", + "data": { + "window_sensor_entity_id": "Identyfikator encji sensora otwarcia okna", + "window_delay": "Zwłoka otwarcia okna (w sek.)", + "window_off_delay": "Zwłoka zamknięcia okna (w sek.)", + "window_auto_open_threshold": "Próg spadku temperatury dla automatycznej detekcji otwarcia okna (w °/godz.)", + "window_auto_close_threshold": "Próg wzrostu temperatury dla automatycznej detekcji zamknięcia okna (w °/godz.)", + "window_auto_max_duration": "Maksymalny czas trwania detekcji otwarcia okna (w min)", + "use_window_central_config": "Użyj centralnej konfiguracji okna", + "window_action": "Akcja" + }, + "data_description": { + "window_sensor_entity_id": "Pozostaw puste, jeśli nie chcesz używać sensora okna i chcesz użyć automatycznej detekcji", + "window_delay": "Zwłoka (w sek.) przed detekcją otwarcia okna", + "window_off_delay": "Zwłoka (w sek.) przed detekcją zamknięcia okna. Pozostaw puste, aby użyć tej samej wartości, jak dla detekcji otwarcia okna", + "window_auto_open_threshold": "Zalecana wartość: między 3 a 10. Pozostaw puste, jeśli automatyczna detekcja otwarcia okna nie jest używana.", + "window_auto_close_threshold": "Zalecana wartość: 0. Pozostaw puste, jeśli automatyczna detekcja otwarcia okna nie jest używana", + "window_auto_max_duration": "Zalecana wartość: 60 (jedna godzina). Pozostaw puste, jeśli automatyczna detekcja otwarcia okna nie jest używana", + "use_window_central_config": "Wybierz, aby użyć centralnej konfiguracji okna. Wyłącz wybór, aby użyć konfiguracji okna dedykowanej dla tego termostatu", + "window_action": "Akcja do wykonania, gdy wykryte zostanie otwarte okno" + } + }, + "motion": { + "title": "Ruch - {name}", + "description": "Zarządzanie sensorem ruchu. Presety mogą przełączać się automatycznie w zależności od wykrycia ruchu. Parametry `preset ruchu` i `preset braku ruchu` powinny być ustawione jako odpowiednie presety. ", + "data": { + "motion_sensor_entity_id": "Identyfikator encji sensora ruchu", + "motion_delay": "Zwłoka aktywacji", + "motion_off_delay": "Zwłoka deaktywacji", + "motion_preset": "Preset ruchu", + "no_motion_preset": "Preset braku ruchu", + "use_motion_central_config": "Użyj centralnej konfiguracji ruchu" + }, + "data_description": { + "motion_sensor_entity_id": "Identyfikator encji sensora ruchu", + "motion_delay": "Zwłoka aktywacji (w sek.)", + "motion_off_delay": "Zwłoka deaktywacji (w sek.)", + "motion_preset": "Preset, gdy wykryto ruch", + "no_motion_preset": "Preset, gdy brak wykrycia ruchu", + "use_motion_central_config": "Wybierz, aby użyć centralnej konfiguracji ruchu. Wyłącz wybór, aby użyć konfiguracji ruchu dedykowanej dla tego termostatu" + } + }, + "power": { + "title": "Energia - {name}", + "description": "Atrybuty zarządzania energią.\nWskazują moc i maksymalną moc sensora.\nOkreślają pobór mocy grzejnika, gdy jest włączony.\nWszystkie sensory i urządzenia powinny mieć tę samą jednostkę mocy (kW lub W)", + "data": { + "power_sensor_entity_id": "Moc", + "max_power_sensor_entity_id": "Moc maksymalna", + "power_temp": "Temperatura progowa, redukująca zużycie energii", + "use_power_central_config": "Użyj centralnej konfiguracji energii" + }, + "data_description": { + "power_sensor_entity_id": "Identyfikator encji sensora energii", + "max_power_sensor_entity_id": "Identyfikator encji sensora energii maksymalnej", + "power_temp": "Temperatura progowa redukująca zużycie energii", + "use_power_central_config": "Wybierz, aby użyć centralnej konfiguracji energii. Wyłącz wybór, aby użyć konfiguracji energii dedykowanej dla tego termostatu" + } + }, + "presence": { + "title": "Obecność - {name}", + "description": "Atrybuty zarządzania obecnością.\nWskazują stan sensora obecności (PRAWDA, gdy wykrywana jest czyjaś obecność) oraz odpowiednie presety temperatury", + "data": { + "presence_sensor_entity_id": "Sensor obecności", + "use_presence_central_config": "Wybierz, aby użyć centralnej konfiguracji temperatury dla wykrytej obecności. Wyłącz wybór, aby użyć wybranych encji temperatury" + }, + "data_description": { + "presence_sensor_entity_id": "Identyfikator encji sensora obecności" + } + }, + "advanced": { + "title": "Zaawansowane - {name}", + "description": "Konfiguracja parametrów zaawansowanych. Pozostaw wartości domyślne, jeśli nie wiesz, co zrobić.\nNieprawidłowa zmiana tych parametrów może prowadzić do błędnej kontroli temperatur lub błędnej regulacji mocy", + "data": { + "minimal_activation_delay": "Minimalna zwłoka aktywacji", + "minimal_deactivation_delay": "Minimalna zwłoka deaktywacji", + "safety_delay_min": "Zwłoka trybu bezpiecznego (w min.)", + "safety_min_on_percent": "Minimalny procent mocy do przejścia w tryb bezpieczny", + "safety_default_on_percent": "Procent mocy do użycia trybu bezpiecznego", + "repair_incorrect_state": "Napraw nieprawidłowy stan", + "use_advanced_central_config": "Użyj zaawansowanej konfiguracji centralnej" + }, + "data_description": { + "minimal_activation_delay": "Zwłoka (w sek.), w czasie której urządzenie nie zostanie aktywowane", + "minimal_deactivation_delay": "Zwłoka (w sek.), w czasie której urządzenie będzie utrzymywane w stanie gotowości", + "safety_delay_min": "Maksymalna dopuszczalna zwłoka (w min.) między dwoma pomiarami temperatury. Powyżej tej zwłoki termostat przełączy się w stan wyłączenia trybu bezpiecznego.", + "safety_min_on_percent": "Minimalna wartość procentowa ogrzewania dla załączenia trybu bezpiecznego. Poniżej tej wartości termostat nie załączy trybu bezpiecznego.", + "safety_default_on_percent": "Domyślna wartość procentowa mocy grzewczej w trybie bezpiecznym. Ustaw na 0, aby wyłączyć ogrzewanie w bezpiecznym presecie", + "repair_incorrect_state": "Automatycznie napraw nieprawidłowy stan jednostek bazowych po wykryciu. Funkcja ponownie wyśle żądane polecenie, jeśli stan urządzenia bazowego nie odpowiada oczekiwanemu stanowi.", + "use_advanced_central_config": "Wybierz, aby użyć zaawansowanej konfiguracji centralnej. Wyłącz wybór, aby użyć konfiguracji zaawansowanej dedykowanej dla tego termostatu" + } + }, + "central_boiler": { + "title": "Sterowanie kotłem centralnym - {name}", + "description": "Wprowadź polecenia załączające i wyłączające kocioł centralny. Polecenia muszą mieć następujacy format: `id_encji/nazwa_polecenia[/atrybut:wartość]` (/atrybut:wartość jest opcjonalny)\nNa przykład:\n- aby załączyć przełącznik: `switch.sterownik_pieca/switch.turn_on`\n- aby wyłączyć przełącznik: `switch.sterownik_pieca/switch.turn_off`\n- aby zaprogramować temperaturę kotła na 25° i w ten sposób wymusić jego zapłon: `climate.termostat_pieca/climate.set_temperature/temperature:25`\n- aby ustawić temperaturę kotła na 10° i w ten sposób wymusić jego wygaszenie: `climate.termostat_pieca/climate.set_temperature/temperature:10`", + "data": { + "central_boiler_activation_service": "Polecenie załączenia", + "central_boiler_deactivation_service": "Polecenie wyłączenia", + "central_boiler_activation_delay_sec": "Opóźnienie załączenia (w sekundach)", + "keep_alive_boiler_delay_sec": "Opóźnienie keep-alive (sekundy)" + }, + "data_description": { + "central_boiler_activation_service": "Polecenie załączenia kotła centralnego w formacie: `id_encji/nazwa_polecenia[/atrybut:wartość]`", + "central_boiler_deactivation_service": "Polecenie wyłączenia kotła centralnego w formacie: `id_encji/nazwa_polecenia[/atrybut:wartość]`", + "central_boiler_activation_delay_sec": "Opóźnienie (w sekundach) przed załączeniem kotła centralnego po tym, jak VTherm zażądał włączenia", + "keep_alive_boiler_delay_sec": "Opóźnienie w sekundach do okresowego ponownego wysłania polecenia kotła, jeśli keep-alive jest włączony. Ustaw na 0, aby wyłączyć keep-alive." + } + }, + "valve_regulation": { + "title": "Autoregulacja zaworu - {name}", + "description": "Konfiguracja autoregulacji z bezpośrednim sterowaniem zaworem", + "data": { + "offset_calibration_entity_ids": "Encje kalibracji", + "opening_degree_entity_ids": "Encje stopnia otwarcia", + "closing_degree_entity_ids": "Encje stopnia zamknięcia", + "proportional_function": "Algorytm", + "opening_threshold_degree": "Próg otwarcia zaworu", + "min_opening_degrees": "Minimalny stopień otwarcia", + "max_opening_degrees": "Maksymalny stopień otwarcia", + "max_closing_degree": "Maksymalny stopień zamknięcia" + }, + "data_description": { + "offset_calibration_entity_ids": "Lista encji kalibracji. Ustaw, jeśli zawór wyposażony jest w encję korekty regulacji. Dla każdego termostatu powinna istnieć maksymalnie jedna taka encja", + "opening_degree_entity_ids": "Lista encji 'stopnia otwarcia'. Powinna istnieć jedna dla każdego termostatu głównego.", + "closing_degree_entity_ids": "Lista encji 'stopnia zamknięcia'. Ustaw, jeśli zawór wyposażony jest w encję korekty regulacji. Dla każdego termostatu powinna istnieć maksymalnie jedna taka encja", + "proportional_function": "Algorytm do użycia (inne algorytmy można zainstalować jako {plugins_link})", + "opening_threshold_degree": "Próg otwarcia zaworu, poniżej którego zawór powinien być uważany za zamknięty (wówczas będzie obowiązywał parametr maksymalnego stopnia zamknięcia)", + "min_opening_degrees": "Minimalne wartości stopnia otwarcia zaworu dla każdego urządzenia, oddzielone przecinkiem. Domyślnie: 0. Przykład: 20, 25, 30", + "max_opening_degrees": "Maksymalna wartość stopnia otwarcia zaworu. Zawór nigdy się nie otworzy więcej niż ta wartość", + "max_closing_degree": "Maksymalna wartość stopnia zamknięcia zaworu. Powyżej tej wartości zawór nigdy nie zostanie zamknięty. Ustaw wartość tego parametru na `100`, aby całkowicie zamknąć zawór, jeśli ogrzewanie jest już niepotrzebne" + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - Ogólne - {name}", + "description": "Ogólne parametry nauki Auto TPI.", + "data": { + "auto_tpi_enable_update_config": "Aktualizuj konfigurację", + "auto_tpi_enable_notification": "Powiadomienia", + "auto_tpi_keep_ext_learning": "Kontynuuj naukę współczynnika zewnętrznego", + "auto_tpi_continuous_learning": "Ciągłe uczenie", + "heater_heating_time": "Czas nagrzewania się grzejnika (w min.)", + "heater_cooling_time": "Czas wychładzania się grzejnika (w min.)", + "auto_tpi_max_coef_int": "Maksymalna wartość współczynnika wewnętrznego", + "auto_tpi_heating_rate": "Szybkość nagrzewania (°C/h)", + "auto_tpi_cooling_rate": "Szybkość wystudzania (°C/h)" + }, + "data_description": { + "auto_tpi_enable_update_config": "Automatycznie aktualizuj konfigurację o wyuczone współczynniki", + "auto_tpi_enable_notification": "Wyślij powiadomienie, gdy nauka zostanie zakończona", + "auto_tpi_keep_ext_learning": "Nauka wpółczynnika zewnętrznego będzie kontynuowana nawet po osiągnieciu 50 cykli, dopóki współczynnik wewnętrzny nie ustabilizuje się.", + "auto_tpi_continuous_learning": "Po zakończeniu procesu nauki system będzie obliczał i aplikował w tle nowe wartości współczynników (zalecana metoda EMA).", + "heater_heating_time": "Czas osiągnięcia pełnej mocy (w min.)", + "heater_cooling_time": "Czas wychłodzenia po zatrzymaniu (w min.)", + "auto_tpi_max_coef_int": "Maksymalna dopuszczalna wartość współczynnika wewnętrznego" + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - Średnia - {name}", + "description": "Parametry metody uśredniania.", + "data": { + "auto_tpi_avg_initial_weight": "Waga początkowa" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "Waga początkowej/poprzedniej wartości w obliczaniu średniej" + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EMA - {name}", + "description": "Parametry metody EMA.\n\nZalecenia:\n| Sytuacja | Alfa (ema_alpha) | Tempo rozkładu (ema_decay_rate) |\n| :--- | :--- | :--- |\n| Początkowe uczenie | 0.15 | 0.08 |\n| Dokładne uczenie | 0.08 | 0.12 |\n| Ciągłe uczenie | 0.05 | 0.02 |", + "data": { + "auto_tpi_ema_alpha": "Alfa", + "auto_tpi_ema_decay_rate": "Wpółczynnik rozkładu" + }, + "data_description": { + "auto_tpi_ema_alpha": "Współczynnik wygładzania (0-1). Im wyższy, tym szybsza adaptacja", + "auto_tpi_ema_decay_rate": "Tempo, w jakim rozkład alfa maleje w czasie (stabilizacja)" + } + }, + "heating_failure_detection": { + "title": "Wykrywanie awarii ogrzewania", + "description": "Konfiguracja funkcji wykrywania awarii ogrzewania. Wykrywa anomalie, gdy ogrzewanie jest oczekiwane, ale temperatura nie wzrasta, lub gdy ogrzewanie jest wyłączone, ale temperatura nadal rośnie.", + "data": { + "use_heating_failure_detection_feature": "Włącz wykrywanie awarii ogrzewania", + "use_heating_failure_detection_central_config": "Użyj centralnej konfiguracji wykrywania awarii", + "heating_failure_threshold": "Próg awarii ogrzewania", + "cooling_failure_threshold": "Próg awarii chłodzenia", + "heating_failure_detection_delay": "Opóźnienie wykrywania (minuty)", + "temperature_change_tolerance": "Tolerancja zmiany temperatury (°C)", + "failure_detection_enable_template": "Szablon włączenia wykrywania" + }, + "data_description": { + "use_heating_failure_detection_feature": "Włącz wykrywanie anomalii ogrzewania dla VTherm z TPI", + "use_heating_failure_detection_central_config": "Zaznacz, aby użyć centralnej konfiguracji wykrywania awarii. Odznacz, aby użyć specyficznych parametrów dla tego VTherm", + "heating_failure_threshold": "Próg procentowy włączenia, powyżej którego ogrzewanie powinno powodować wzrost temperatury (0.9 = 90%)", + "cooling_failure_threshold": "Próg procentowy włączenia, poniżej którego temperatura nie powinna wzrastać (0.0 = 0%)", + "heating_failure_detection_delay": "Czas w minutach do odczekania przed sprawdzeniem, czy temperatura zmieniła się zgodnie z oczekiwaniami", + "temperature_change_tolerance": "Minimalna zmiana temperatury w stopniach uznawana za znaczącą. Mniejsze zmiany są ignorowane w celu filtrowania szumu czujnika (domyślnie: 0.5°C)", + "failure_detection_enable_template": "Opcjonalny szablon Jinja2, który musi zwrócić True, aby włączyć wykrywanie. Przydatne do tymczasowego wyłączenia wykrywania, gdy zewnętrzne źródło ciepła jest aktywne (np. `is_state('binary_sensor.piec_na_drewno', 'off')` w podwójnych nawiasach klamrowych). Jeśli pusty, wykrywanie jest zawsze włączone." + } + } + }, + "error": { + "unknown": "Nieoczekiwany błąd", + "unknown_entity": "Nieznany identyfikator encji", + "window_open_detection_method": "Może być używana tylko jedna metoda detekcji otwarcia okna. Użyj sensora okna lub automatycznej detekcji przekroczenia progu temperatury, ale nie używaj obu tych metod jednocześnie.", + "no_central_config": "Nie możesz wybrać konfiguracji centralnej z powodu jej braku. Aby jej użyć, najpierw musisz utworzyć termostat VTherm typu `Konfiguracja Centralna`.", + "service_configuration_format": "Błędny format konfiguracji polecenia lub usługi", + "valve_regulation_nb_entities_incorrect": "Liczba encji zaworu wymagana do jego regulacji powinna być równa ilości zaworów", + "min_opening_degrees_format": "Oczekiwany separator przecinkowy listy lub dodatnia liczba całkowita. Przykład: 20, 25, 30", + "max_opening_degrees_format": "Oczekiwana jest lista dodatnich liczb całkowitych między 0 a 100 oddzielonych przecinkami. Przykład: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Dla każdego urządzenia bazowego max_opening_degrees musi być ściśle większy niż min_opening_degrees. Sprawdź swoją konfigurację.", + "vswitch_configuration_incorrect": "Błąd konfiguracji poleceń własnych. Wymagane są one dla urządzeń nie będących przełącznikami. Ich format musi być następujący: `service_name[/attribute:value]`. Więcej informacji znajdziesz w dokumentacji" + }, + "abort": { + "already_configured": "Urządzenie jest już skonfigurowane" + } + }, + "selector": { + "thermostat_type": { + "options": { + "thermostat_central_config": "Konfiguracja Centralna", + "thermostat_over_switch": "Termostat na Przełączniku", + "thermostat_over_climate": "Termostat na Klimacie", + "thermostat_over_valve": "Termostat na Zaworze" + } + }, + "auto_regulation_mode": { + "options": { + "auto_regulation_slow": "Powolna", + "auto_regulation_strong": "Silna", + "auto_regulation_medium": "Średnia", + "auto_regulation_light": "Słaba", + "auto_regulation_expert": "Ekspert", + "auto_regulation_none": "Brak autoregulacji", + "auto_regulation_valve": "Bezpośrednie sterowanie zaworu" + } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "Brak autowentylacji", + "auto_fan_low": "Niski", + "auto_fan_medium": "Średni", + "auto_fan_high": "Wysoki", + "auto_fan_turbo": "Turbo" + } + }, + "window_action": { + "options": { + "window_turn_off": "Wyłączone", + "window_fan_only": "Tylko wentylacja", + "window_frost_temp": "Ochrona przed mrozem", + "window_eco_temp": "Eko" + } + }, + "presets": { + "options": { + "frost": "Ochrona przed mrozem", + "eco": "Eko", + "comfort": "Komfort", + "boost": "Boost" + } + }, + "auto_start_stop": { + "options": { + "auto_start_stop_none": "Brak autoSTART/autoSTOP", + "auto_start_stop_very_slow": "Bardzo powolna detekcja", + "auto_start_stop_slow": "Powolna detekcja", + "auto_start_stop_medium": "Średnia detekcja", + "auto_start_stop_fast": "Szybka detekcja" + } + }, + "auto_tpi_calculation_method": { + "options": { + "average": "Średnia", + "ema": "Zmienna Średnia Wykładnicza (EMA)" + } + } + }, + "entity": { + "climate": { + "versatile_thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "power": "Ograniczony", + "safety": "Bezpieczny", + "none": "Ręczny", + "frost": "Ochrona przed mrozem" + } + } + } + } + }, + "number": { + "frost_temp": { + "name": "Ochrona przed mrozem" + }, + "eco_temp": { + "name": "Eko" + }, + "comfort_temp": { + "name": "Komfort" + }, + "boost_temp": { + "name": "Boost" + }, + "frost_ac_temp": { + "name": "Ochrona przd mrozem AC" + }, + "eco_ac_temp": { + "name": "Eko AC" + }, + "comfort_ac_temp": { + "name": "Komfort AC" + }, + "boost_ac_temp": { + "name": "Boost AC" + }, + "frost_away_temp": { + "name": "Ochrona przed mrozem w trybie poza domem" + }, + "eco_away_temp": { + "name": "Eko poza domem" + }, + "comfort_away_temp": { + "name": "Komfort poza domem" + }, + "boost_away_temp": { + "name": "Boost poza domem" + }, + "eco_ac_away_temp": { + "name": "Eko AC poza domem" + }, + "comfort_ac_away_temp": { + "name": "Komfort AC poza domem" + }, + "boost_ac_away_temp": { + "name": "Boost AC poza domem" + } + } + }, + "services": { + "set_auto_tpi_mode": { + "name": "Tryb Auto TPI", + "description": "Załącz lub wyłącz tryb nauki Auto TPI", + "fields": { + "auto_tpi_mode": { + "name": "Tryb Auto TPI", + "description": "Załącza (`true`) lub wyłącza (`false`) proces uczenia Auto TPI" + }, + "reinitialise": { + "name": "Zresetuj dane wyuczone", + "description": "Resetuje wszystkie dane wyuczone podczas aktywności trybu Auto TPI (domyślnie `true`)" + } + } + }, + "reset_auto_tpi_capacities": { + "name": "Reset wydajności Auto TPI", + "description": "Zresetuj zapamiętane maksymalne wydajności grzania i chłodzenia do 1,0 °C/h. Zapamiętane współczynniki NIE są resetowane." + }, + "auto_tpi_calibrate_capacity": { + "name": "Kalibracja wydajności Auto TPI", + "description": "Skalibruj wydajność ogrzewania/chłodzenia (w °C/godzinę) za pomocą regresji liniowej na danych historycznych obiektu. Wyuczone współczynniki NIE są aktualizowane.", + "fields": { + "start_date": { + "name": "Historia od dnia", + "description": "Data, od której ma zostać pobrana historia (domyślnie: 30 dni temu)." + }, + "end_date": { + "name": "Historia do dnia", + "description": "Data, do której ma zostać pobrana historia (domyślnie: dzisiaj)." + }, + "hvac_mode": { + "name": "Tryb HVAC", + "description": "Który tryb ma być kalibrowany (grzanie lub chłodzenie)." + }, + "save_to_config": { + "name": "Zapis w konfiguracji", + "description": "Zastosuj obliczone wydajności do konfiguracji Auto TPI." + }, + "min_power_threshold": { + "name": "Próg mocy minimalnej", + "description": "Minimalny procent mocy, przy którym cykl jest uznawany za nasycony (domyślnie: 95%). Niższe wartości uwzględniają więcej cykli, ale mogą być mniej dokładne." + } + } + }, + "set_tpi_parameters": { + "name": "Ustawienia parametrów TPI", + "description": "Zmień parametry TPI", + "fields": { + "minimal_activation_delay": { + "name": "Minimalna zwłoka aktywacji", + "description": "Zwłoka (w sek.) aktywacji urządzeń" + }, + "minimal_deactivation_delay": { + "name": "Minimalna zwłoka deaktywacji", + "description": "Zwłoka (w sek.) deaktywacji urządzeń (czas podtrzymania działania)" + }, + "tpi_threshold_low": { + "name": "Dolny próg TPI", + "description": "Próg w °, poniżej którego algorytm TPI zostanie załączony. `0` oznacza brak progu." + }, + "tpi_threshold_high": { + "name": "Górny próg TPI", + "description": "Próg w °, powyżej którego algorytm TPI zostanie wyłączony. `0` oznacza brak progu." + } + } + }, + "lock": { + "name": "Blokada", + "description": "Blokada termostatu przed zmianą jego konfiguracji przez użytkownika lub automatyzacje.", + "fields": { + "code": { + "name": "Kod blokady", + "description": "Opcjonalny kod blokady" + } + } + }, + "unlock": { + "name": "Odblokowanie", + "description": "Odblokowuje termostat, umozliwiając zmianę jego konfiguracji.", + "fields": { + "code": { + "name": "Kod blokady", + "description": "Opcjonalny kod blokady" + } + } + } + }, + "exceptions": { + "auto_tpi_learning_stopped": { + "message": "Nauka Auto TPI dla {name} została zatrzymana z powodu 3-ech kolejnych błędów. Powód: {reason}. Sprawdź swoją konfigurację." + } + } +} \ No newline at end of file diff --git a/custom_components/versatile_thermostat/translations/ru.json b/custom_components/versatile_thermostat/translations/ru.json new file mode 100644 index 0000000..4f0dc1c --- /dev/null +++ b/custom_components/versatile_thermostat/translations/ru.json @@ -0,0 +1,794 @@ +{ + "title": "Versatile Thermostat", + "config": { + "flow_title": "Конфигурация универсального термостата", + "step": { + "user": { + "title": "Тип универсального термостата", + "data": { + "thermostat_type": "Тип термостата" + }, + "data_description": { + "thermostat_type": "Возможен только один тип центральной конфигурации" + } + }, + "menu": { + "title": "Меню", + "description": "Настройте ваш термостат. Вы сможете завершить конфигурацию, когда все необходимые параметры будут введены.", + "menu_options": { + "main": "Основные атрибуты", + "central_boiler": "Центральный котел", + "type": "Управляемые устройства", + "tpi": "Параметры TPI", + "features": "Функции", + "presets": "Предустановки", + "window": "Обнаружение окон", + "motion": "Обнаружение движения", + "power": "Управление питанием", + "presence": "Обнаружение присутствия", + "advanced": "Расширенные параметры", + "auto_start_stop": "Авто старт и стоп", + "valve_regulation": "Конфигурация регулировки клапана", + "sync_device_internal_temp": "Синхронизация температуры устройства", + "lock": "Управление блокировкой", + "finalize": "Все готово", + "configuration_not_complete": "Конфигурация не завершена", + "heating_failure_detection": "Обнаружение сбоев отопления" + } + }, + "lock": { + "title": "Управление блокировкой", + "description": "Функция блокировки предотвращает изменения в конфигурации термостата из пользовательского интерфейса или автоматизаций, сохраняя при этом работоспособность термостата.", + "data": { + "use_lock_central_config": "Использовать центральную конфигурацию блокировки", + "lock_users": "Блокировка будет предотвращать команды от пользователей", + "lock_automations": "Блокировка будет предотвращать команды от автоматизаций и интеграций (например, планировщика)" + }, + "data_description": { + "use_lock_central_config": "Отметьте, чтобы использовать центральную конфигурацию блокировки. Снимите флажок, чтобы использовать специальную конфигурацию блокировки для этого VTherm", + "lock_code": "Пин-код для разблокировки термостата (4 цифры)" + } + }, + "main": { + "title": "Добавить новый универсальный термостат", + "description": "Основные обязательные атрибуты", + "data": { + "name": "Название", + "thermostat_type": "Тип термостата", + "temperature_sensor_entity_id": "Температура в помещении", + "last_seen_temperature_sensor_entity_id": "Время последнего измерения температуры", + "external_temperature_sensor_entity_id": "ID датчика уличной температуры", + "cycle_min": "Длительность цикла (минуты)", + "temp_min": "Минимальная допустимая температура", + "temp_max": "Максимальная допустимая температура", + "step_temperature": "Шаг температуры", + "device_power": "Мощность устройства", + "use_central_mode": "Включить управление через центральную сущность (требует центральной конфигурации). Отметьте для управления VTherm через сущности центральной конфигурации.", + "use_main_central_config": "Использовать дополнительную центральную конфигурацию. Отметьте для использования центральной конфигурации (уличная температура, мин, макс, шаг, ...).", + "used_by_controls_central_boiler": "Используется центральным котлом. Отметьте, если этот VTherm должен управлять центральным котлом" + }, + "data_description": { + "cycle_min": "Минимальная длительность одного цикла обогрева/охлаждения в минутах, чтобы избежать слишком частого переключения", + "temp_min": "Минимальная допустимая температура для термостата", + "temp_max": "Максимальная допустимая температура для термостата", + "step_temperature": "Шаг температуры для установки целевой температуры", + "temperature_sensor_entity_id": "ID датчика температуры в помещении", + "last_seen_temperature_sensor_entity_id": "ID датчика времени последнего измерения температуры. Должен быть датчиком datetime", + "external_temperature_sensor_entity_id": "ID датчика уличной температуры. Не используется, если выбрана центральная конфигурация" + } + }, + "features": { + "title": "Функции", + "description": "Функции термостата", + "data": { + "use_window_feature": "Использовать обнаружение окон", + "use_motion_feature": "Использовать обнаружение движения", + "use_power_feature": "Использовать управление питанием", + "use_presence_feature": "Использовать обнаружение присутствия", + "use_central_boiler_feature": "Использовать центральный котел. Отметьте для добавления управления вашим центральным котлом. После выбора этой опции вам нужно будет настроить VTherm, который будет управлять центральным котлом. Если один VTherm требует обогрев, котел включится. Если ни один VTherm не требует обогрев, котел выключится. Команды для включения/выключения центрального котла задаются на соответствующей странице конфигурации", + "use_auto_start_stop_feature": "Использовать функцию авто старта и остановки", + "use_heating_failure_detection_feature": "Использовать обнаружение сбоев отопления" + } + }, + "type": { + "title": "Связанные сущности", + "description": "Атрибуты связанных сущностей", + "data": { + "underlying_entity_ids": "Устройство(а) для управления", + "heater_keep_alive": "Интервал поддержания переключателя (секунды)", + "proportional_function": "Алгоритм", + "ac_mode": "Режим кондиционера", + "sync_device_internal_temp": "Синхронизация внутренней температуры устройства", + "auto_regulation_mode": "Саморегулирование", + "auto_regulation_dtemp": "Порог регулирования", + "auto_regulation_periode_min": "Минимальный период регулирования", + "auto_regulation_use_device_temp": "Использовать внутреннюю температуру устройства", + "inverse_switch_command": "Инвертировать команду переключателя", + "auto_fan_mode": "Авто режим вентилятора", + "on_command_text": "Настройка команды включения", + "vswitch_on_command": "Опциональные команды включения", + "off_command_text": "Настройка команды выключения", + "vswitch_off_command": "Опциональные команды выключения" + }, + "data_description": { + "underlying_entity_ids": "Устройство(а) для управления - требуется 1", + "heater_keep_alive": "Опциональный интервал обновления состояния переключателя. Оставьте пустым, если не требуется.", + "proportional_function": "Алгоритм для использования (другие алгоритмы можно установить как {plugins_link})", + "ac_mode": "Использовать режим кондиционера (AC)", + "sync_device_internal_temp": "Конфигурация для синхронизации внутренней температуры подчиненных устройств с выбранными сущностями", + "auto_regulation_mode": "Автоматическая корректировка целевой температуры", + "auto_regulation_dtemp": "Порог в ° (или % для клапана), ниже которого изменение температуры не будет отправлено", + "auto_regulation_periode_min": "Время в минутах между двумя обновлениями регулирования", + "auto_regulation_use_device_temp": "Использовать внутренний датчик температуры устройства для ускорения саморегулирования (в большинстве случаев не требуется)", + "inverse_switch_command": "Для переключателей с пилотным проводом и диодом может потребоваться инвертировать команду", + "auto_fan_mode": "Автоматически активировать вентилятор при необходимости сильного обогрева/охлаждения", + "on_command_text": "Для устройств типа `select` или `climate` необходимо настроить команды." + } + }, + "tpi": { + "title": "TPI", + "description": "Атрибуты Time Proportional Integral", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "tpi_threshold_low": "Нижний порог TPI", + "tpi_threshold_high": "Верхний порог TPI", + "use_tpi_central_config": "Использовать центральную конфигурацию TPI", + "minimal_activation_delay": "Минимальная задержка активации", + "minimal_deactivation_delay": "Минимальная задержка деактивации" + }, + "data_description": { + "tpi_coef_int": "Коэффициент для дельты внутренней температуры", + "tpi_coef_ext": "Коэффициент для дельты внешней температуры", + "tpi_threshold_low": "Порог в °, ниже которого алгоритм TPI будет включен. 0 означает отсутствие порога", + "tpi_threshold_high": "Порог в °, выше которого алгоритм TPI будет выключен. 0 означает отсутствие порога", + "use_tpi_central_config": "Отметьте для использования центральной конфигурации TPI. Снимите для использования специфичной конфигурации TPI для этого VTherm", + "minimal_activation_delay": "Задержка в секундах, ниже которой оборудование не будет активировано", + "minimal_deactivation_delay": "Задержка в секундах, ниже которой оборудование будет оставаться активным" + } + }, + "presets": { + "title": "Предустановки", + "description": "Выберите, будет ли термостат использовать центральные предустановки - снимите для использования собственных предустановок термостата", + "data": { + "use_presets_central_config": "Использовать центральную конфигурацию предустановок" + } + }, + "window": { + "title": "Управление окнами", + "description": "Управление открытыми окнами.\nВы также можете настроить автоматическое обнаружение открытых окон на основе снижения температуры", + "data": { + "window_sensor_entity_id": "ID датчика окна", + "window_delay": "Задержка датчика 'включено' (секунды)", + "window_off_delay": "Задержка датчика 'выключено' (секунды)", + "window_auto_open_threshold": "Порог снижения температуры для автоматического обнаружения открытого окна (в °/час)", + "window_auto_close_threshold": "Порог повышения температуры для окончания автоматического обнаружения (в °/час)", + "window_auto_max_duration": "Максимальная длительность автоматического обнаружения открытого окна (в мин)", + "use_window_central_config": "Использовать центральную конфигурацию окон", + "window_action": "Действие" + }, + "data_description": { + "window_sensor_entity_id": "Оставьте пустым, если датчик окна не используется и для использования автоматического обнаружения", + "window_delay": "Задержка в секундах перед учетом обнаружения датчика 'включено'", + "window_off_delay": "Задержка в секундах перед учетом обнаружения датчика 'выключено'. Оставьте пустым для использования того же значения, что и для задержки включения", + "window_auto_open_threshold": "Рекомендуемое значение: между 3 и 10. Оставьте пустым, если автоматическое обнаружение не используется", + "window_auto_close_threshold": "Рекомендуемое значение: 0. Оставьте пустым, если автоматическое обнаружение не используется", + "window_auto_max_duration": "Рекомендуемое значение: 60 (один час). Оставьте пустым, если автоматическое обнаружение не используется", + "use_window_central_config": "Выберите для использования центральной конфигурации окон. Снимите для использования специфичной конфигурации окон для этого VTherm", + "window_action": "Действие при обнаружении открытого окна" + } + }, + "motion": { + "title": "Управление движением", + "description": "Управление датчиком движения. Предустановка может автоматически переключаться в зависимости от обнаружения движения\nmotion_preset и no_motion_preset должны быть установлены в соответствующие имена предустановок", + "data": { + "motion_sensor_entity_id": "ID датчика движения", + "motion_delay": "Задержка активации", + "motion_off_delay": "Задержка деактивации", + "motion_preset": "Предустановка при движении", + "no_motion_preset": "Предустановка без движения", + "use_motion_central_config": "Использовать центральную конфигурацию движения" + }, + "data_description": { + "motion_sensor_entity_id": "ID сущности датчика движения", + "motion_delay": "Задержка активации движения (секунды)", + "motion_off_delay": "Задержка деактивации движения (секунды)", + "motion_preset": "Предустановка для использования при обнаружении движения", + "no_motion_preset": "Предустановка для использования при отсутствии движения", + "use_motion_central_config": "Отметьте для использования центральной конфигурации движения. Снимите для использования специфичной конфигурации движения для этого VTherm" + } + }, + "power": { + "title": "Управление питанием", + "description": "Атрибуты управления питанием.\nУказывает датчики мощности и максимальной мощности вашего дома.\nУкажите потребляемую мощность обогревателя при включении.\nВсе датчики и мощность устройства должны использовать одинаковые единицы измерения (кВт или Вт)", + "data": { + "power_sensor_entity_id": "Мощность", + "max_power_sensor_entity_id": "Максимальная мощность", + "power_temp": "Температура снижения мощности", + "use_power_central_config": "Использовать центральную конфигурацию питания" + }, + "data_description": { + "power_sensor_entity_id": "ID датчика мощности", + "max_power_sensor_entity_id": "ID датчика максимальной мощности", + "power_temp": "Температура для снижения мощности", + "use_power_central_config": "Отметьте для использования центральной конфигурации питания. Снимите для использования специфичной конфигурации питания для этого VTherm" + } + }, + "presence": { + "title": "Управление присутствием", + "description": "Атрибуты управления присутствием.\nУказывает датчик присутствия в вашем доме (true если кто-то присутствует) и соответствующую настройку температурной предустановки", + "data": { + "presence_sensor_entity_id": "Датчик присутствия", + "use_presence_central_config": "Использовать центральную конфигурацию температуры присутствия. Снимите для использования специфичных температурных сущностей" + }, + "data_description": { + "presence_sensor_entity_id": "ID датчика присутствия" + } + }, + "advanced": { + "title": "Расширенные параметры", + "description": "Конфигурация расширенных параметров. Оставьте значения по умолчанию, если не уверены в своих действиях.\nЭти параметры могут привести к очень плохому контролю температуры или плохому регулированию мощности", + "data": { + "safety_delay_min": "Защитная задержка (в минутах)", + "safety_min_on_percent": "Минимальный процент мощности для включения защитного режима", + "safety_default_on_percent": "Процент мощности для использования в защитном режиме", + "repair_incorrect_state": "Исправить неправильное состояние", + "use_advanced_central_config": "Использовать центральную расширенную конфигурацию" + }, + "data_description": { + "safety_delay_min": "Максимально допустимая задержка в минутах между двумя измерениями температуры. Выше этой задержки термостат перейдет в защитный режим выключения", + "safety_min_on_percent": "Минимальное значение процента нагрева для активации защитной предустановки. Ниже этого значения процента мощности термостат не перейдет в защитную предустановку", + "safety_default_on_percent": "Значение процента мощности нагрева по умолчанию в защитной предустановке. Установите 0 для выключения обогревателя в защитной предустановке", + "repair_incorrect_state": "Автоматически исправить неправильное состояние базовых объектов при обнаружении. Функция повторно отправит желаемую команду, если состояние базового устройства не соответствует ожидаемому состоянию.", + "use_advanced_central_config": "Отметьте для использования центральной расширенной конфигурации. Снимите для использования специфичной расширенной конфигурации для этого VTherm" + } + }, + "central_boiler": { + "title": "Управление центральным котлом", + "description": "Введите сервисы для вызова включения/выключения центрального котла. Вызываемый сервис должен быть отформатирован следующим образом: `entity_id/service_name[/attribute:value]` (/attribute:value опционально)\nНапример:\n- для включения переключателя: `switch.controle_chaudiere/switch.turn_on`\n- для выключения переключателя: `switch.controle_chaudiere/switch.turn_off`\n- для программирования котла на 25° и принудительного включения: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- для отправки 10° котлу и принудительного выключения: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`", + "data": { + "central_boiler_activation_service": "Команда включения", + "central_boiler_deactivation_service": "Команда выключения", + "central_boiler_activation_delay_sec": "Задержка включения (в секундах)", + "keep_alive_boiler_delay_sec": "Задержка keep-alive (секунды)" + }, + "data_description": { + "central_boiler_activation_service": "Команда включения центрального котла в формате entity_id/service_name[/attribut:valeur]", + "central_boiler_deactivation_service": "Команда выключения центрального котла в формате entity_id/service_name[/attribut:valeur]", + "central_boiler_activation_delay_sec": "Задержка включения (в секундах) перед включением центрального котла после того, как VTherm запросил включение", + "keep_alive_boiler_delay_sec": "Задержка в секундах для периодической повторной отправки команды котла, если keep-alive включен. Установите 0, чтобы отключить keep-alive." + } + }, + "valve_regulation": { + "title": "Саморегулирование с клапаном", + "description": "Конфигурация для саморегулирования с прямым управлением клапаном", + "data": { + "offset_calibration_entity_ids": "Сущности калибровки смещения", + "opening_degree_entity_ids": "Сущности степени открытия", + "closing_degree_entity_ids": "Сущности степени закрытия", + "proportional_function": "Алгоритм", + "opening_threshold_degree": "Пороговое значение открытия", + "min_opening_degrees": "Минимальные степени открытия", + "max_opening_degrees": "Максимальная степень открытия", + "max_closing_degree": "Максимальная степень закрытия" + }, + "data_description": { + "offset_calibration_entity_ids": "Список сущностей 'калибровки смещения'. Установите, если ваш TRV имеет такую сущность для лучшего регулирования. Должна быть одна для каждой управляющей климатической сущности", + "opening_degree_entity_ids": "Список сущностей 'степени открытия'. Должна быть одна для каждой управляющей климатической сущности", + "closing_degree_entity_ids": "Список сущностей 'степени закрытия'. Установите, если ваш TRV имеет такую сущность для лучшего регулирования. Должна быть одна для каждой управляющей климатической сущности", + "proportional_function": "Алгоритм для использования (другие алгоритмы можно установить как {plugins_link})", + "opening_threshold_degree": "Значение открытия клапана, ниже которого клапан должен считаться закрытым (и тогда будет применено 'max_closing_degree')", + "min_opening_degrees": "Минимальное значение степени открытия для каждого управляющего устройства, через запятую. По умолчанию 0. Пример: 20, 25, 30", + "max_opening_degrees": "Максимальное значение степени открытия для каждого базового устройства, через запятую. По умолчанию 100. Клапан никогда не откроется больше этого значения. Пример: 80, 85, 90", + "max_closing_degree": "Максимальное значение степени закрытия. Клапан никогда не закроется больше этого значения. Установите на 100, чтобы полностью закрыть клапан" + } + }, + "sync_device_internal_temp": { + "title": "Synchronize device internal temperature", + "description": "Configuration to synchronize the internal temperature of the underlying devices with selected entities", + "data": { + "sync_with_calibration": "Apply offset calibration", + "sync_entity_ids": "Entities id used for synchronization" + }, + "data_description": { + "sync_with_calibration": "Check to apply the offset calibration when synchronizing the internal temperature. Uncheck to directly copy the temperature from the selected entities", + "sync_entity_ids": "The list of the entities used to synchronize the internal temperature of the underlying devices. There should be one per underlying device. Should be a calibration entity if checkbox is selected or a temperature entity otherwise. Both are `number` entities." + } + }, + "heating_failure_detection": { + "title": "Обнаружение сбоев отопления", + "description": "Настройка функции обнаружения сбоев отопления. Обнаруживает аномалии, когда ожидается нагрев, но температура не повышается, или когда отопление выключено, но температура продолжает расти.", + "data": { + "use_heating_failure_detection_feature": "Включить обнаружение сбоев отопления", + "use_heating_failure_detection_central_config": "Использовать центральную конфигурацию обнаружения сбоев", + "heating_failure_threshold": "Порог сбоя отопления", + "cooling_failure_threshold": "Порог сбоя охлаждения", + "heating_failure_detection_delay": "Задержка обнаружения (минуты)", + "temperature_change_tolerance": "Допуск изменения температуры (°C)", + "failure_detection_enable_template": "Шаблон включения обнаружения" + }, + "data_description": { + "use_heating_failure_detection_feature": "Включить обнаружение аномалий отопления для VTherm с TPI", + "use_heating_failure_detection_central_config": "Отметьте для использования центральной конфигурации обнаружения сбоев. Снимите отметку для использования специфических параметров для этого VTherm", + "heating_failure_threshold": "Пороговый процент включения, выше которого отопление должно вызывать повышение температуры (0.9 = 90%)", + "cooling_failure_threshold": "Пороговый процент включения, ниже которого температура не должна повышаться (0.0 = 0%)", + "heating_failure_detection_delay": "Время в минутах ожидания перед проверкой, изменилась ли температура как ожидалось", + "temperature_change_tolerance": "Минимальное изменение температуры в градусах, считающееся значительным. Меньшие изменения игнорируются для фильтрации шума датчика (по умолчанию: 0.5°C)", + "failure_detection_enable_template": "Необязательный шаблон Jinja2, который должен вернуть True для включения обнаружения. Полезно для временного отключения обнаружения при активном внешнем источнике тепла (например, `is_state('binary_sensor.wood_stove', 'off')` в двойных фигурных скобках). Если пусто, обнаружение всегда включено." + } + } + }, + "error": { + "unknown": "Неожиданная ошибка", + "unknown_entity": "Неизвестный ID сущности", + "window_open_detection_method": "Должен использоваться только один метод обнаружения открытых окон. Используйте либо датчик окна, либо автоматическое обнаружение через температурный порог, но не оба одновременно", + "no_central_config": "Вы не можете выбрать 'использовать центральную конфигурацию', потому что центральная конфигурация не найдена. Вам нужно создать универсальный термостат типа 'Центральная конфигурация' для его использования.", + "service_configuration_format": "Неверный формат конфигурации сервиса", + "valve_regulation_nb_entities_incorrect": "Количество сущностей клапана для регулирования клапана должно быть равно количеству управляющих устройств", + "sync_device_internal_temp_nb_entities_incorrect": "Количество сущностей для синхронизации внутренней температуры устройства должно быть равно количеству управляющих устройств", + "min_opening_degrees_format": "Ожидается список положительных целых чисел, разделенных запятыми. Пример: 20, 25, 30", + "max_opening_degrees_format": "Ожидается список положительных целых чисел от 0 до 100, разделенных запятыми. Пример: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Для каждого базового устройства max_opening_degrees должен быть строго больше min_opening_degrees. Проверьте вашу конфигурацию.", + "vswitch_configuration_incorrect": "Неверная конфигурация настройки команды. Она обязательна для базовых объектов, которые не являются переключателями, и формат должен быть service_name[/attribute:value]. Дополнительная информация доступна в README" + }, + "abort": { + "already_configured": "Устройство уже настроено" + } + }, + "options": { + "flow_title": "Конфигурация универсального термостата", + "step": { + "user": { + "title": "Тип - {name}", + "data": { + "thermostat_type": "Тип термостата" + }, + "data_description": { + "thermostat_type": "Возможен только один тип центральной конфигурации" + } + }, + "menu": { + "title": "Меню", + "description": "Настройте ваш термостат. Вы сможете завершить конфигурацию, когда все необходимые параметры будут введены.", + "menu_options": { + "main": "Основные атрибуты", + "central_boiler": "Центральный котел", + "type": "Управляемые устройства", + "tpi": "Параметры TPI", + "features": "Функции", + "presets": "Предустановки", + "window": "Обнаружение окон", + "motion": "Обнаружение движения", + "power": "Управление питанием", + "presence": "Обнаружение присутствия", + "advanced": "Расширенные параметры", + "auto_start_stop": "Авто старт и стоп", + "valve_regulation": "Конфигурация регулировки клапана", + "sync_device_internal_temp": "Синхронизация температуры устройства", + "lock": "Управление блокировкой", + "finalize": "Все готово", + "configuration_not_complete": "Конфигурация не завершена", + "heating_failure_detection": "Обнаружение сбоев отопления" + } + }, + "lock": { + "title": "Управление блокировкой - {name}", + "description": "Функция блокировки предотвращает изменения в конфигурации термостата из пользовательского интерфейса или автоматизаций, сохраняя при этом работоспособность термостата.", + "data": { + "use_lock_central_config": "Использовать центральную конфигурацию блокировки", + "lock_users": "Блокировка будет предотвращать команды от пользователей", + "lock_automations": "Блокировка будет предотвращать команды от автоматизаций и интеграций (например, планировщика)" + }, + "data_description": { + "use_lock_central_config": "Отметьте, чтобы использовать центральную конфигурацию блокировки. Снимите флажок, чтобы использовать специальную конфигурацию блокировки для этого VTherm", + "lock_code": "Пин-код для разблокировки термостата (4 цифры)" + } + }, + "main": { + "title": "Основные - {name}", + "description": "Основные обязательные атрибуты", + "data": { + "name": "Название", + "thermostat_type": "Тип термостата", + "temperature_sensor_entity_id": "Температура в помещении", + "last_seen_temperature_sensor_entity_id": "Время последнего измерения температуры", + "external_temperature_sensor_entity_id": "ID датчика уличной температуры", + "cycle_min": "Длительность цикла (минуты)", + "temp_min": "Минимальная допустимая температура", + "temp_max": "Максимальная допустимая температура", + "step_temperature": "Шаг температуры", + "device_power": "Мощность устройства", + "use_central_mode": "Включить управление через центральную сущность (требует центральной конфигурации). Отметьте для управления VTherm через сущности central_mode.", + "use_main_central_config": "Использовать дополнительную центральную конфигурацию. Отметьте для использования центральной конфигурации (уличная температура, мин, макс, шаг, ...).", + "used_by_controls_central_boiler": "Используется центральным котлом. Отметьте, если этот VTherm должен управлять центральным котлом" + }, + "data_description": { + "cycle_min": "Минимальная длительность одного цикла обогрева/охлаждения в минутах, чтобы избежать слишком частого переключения", + "temp_min": "Минимальная допустимая температура для термостата", + "temp_max": "Максимальная допустимая температура для термостата", + "step_temperature": "Шаг температуры для установки целевой температуры", + "temperature_sensor_entity_id": "ID датчика температуры в помещении", + "last_seen_temperature_sensor_entity_id": "ID датчика времени последнего измерения температуры. Должен быть датчиком datetime", + "external_temperature_sensor_entity_id": "ID датчика уличной температуры. Не используется, если выбрана центральная конфигурация" + } + }, + "features": { + "title": "Функции - {name}", + "description": "Функции термостата", + "data": { + "use_window_feature": "Использовать обнаружение окон", + "use_motion_feature": "Использовать обнаружение движения", + "use_power_feature": "Использовать управление питанием", + "use_presence_feature": "Использовать обнаружение присутствия", + "use_central_boiler_feature": "Использовать центральный котел. Отметьте для добавления управления вашим центральным котлом. После выбора этой опции вам нужно будет настроить VTherm, который будет управлять центральным котлом. Если один VTherm требует обогрев, котел включится. Если ни один VTherm не требует обогрев, котел выключится. Команды для включения/выключения центрального котла задаются на соответствующей странице конфигурации", + "use_auto_start_stop_feature": "Использовать функцию авто старта и остановки", + "use_heating_failure_detection_feature": "Использовать обнаружение сбоев отопления" + } + }, + "type": { + "title": "Сущности - {name}", + "description": "Атрибуты связанных сущностей", + "data": { + "underlying_entity_ids": "Устройство(а) для управления", + "heater_keep_alive": "Интервал поддержания переключателя (секунды)", + "proportional_function": "Алгоритм", + "ac_mode": "Режим кондиционера", + "sync_device_internal_temp": "Синхронизация внутренней температуры устройства", + "auto_regulation_mode": "Саморегулирование", + "auto_regulation_dtemp": "Порог регулирования", + "auto_regulation_periode_min": "Минимальный период регулирования", + "auto_regulation_use_device_temp": "Использовать внутреннюю температуру устройства", + "inverse_switch_command": "Инвертировать команду переключателя", + "auto_fan_mode": "Авто режим вентилятора", + "on_command_text": "Настройка команды включения", + "vswitch_on_command": "Опциональные команды включения", + "off_command_text": "Настройка команды выключения", + "vswitch_off_command": "Опциональные команды выключения" + }, + "data_description": { + "underlying_entity_ids": "Устройство(а) для управления - требуется 1", + "heater_keep_alive": "Опциональный интервал обновления состояния переключателя. Оставьте пустым, если не требуется.", + "proportional_function": "Алгоритм для использования (другие алгоритмы можно установить как {plugins_link})", + "ac_mode": "Использовать режим кондиционера (AC)", + "sync_device_internal_temp": "Конфигурация для синхронизации внутренней температуры подчиненных устройств с выбранными сущностями", + "auto_regulation_mode": "Автоматическая корректировка целевой температуры", + "auto_regulation_dtemp": "Порог в ° (или % для клапана), ниже которого изменение температуры не будет отправлено", + "auto_regulation_periode_min": "Время в минутах между двумя обновлениями регулирования", + "auto_regulation_use_device_temp": "Использовать внутренний датчик температуры устройства для ускорения саморегулирования (в большинстве случаев не требуется)", + "inverse_switch_command": "Для переключателей с пилотным проводом и диодом может потребоваться инвертировать команду", + "auto_fan_mode": "Автоматически активировать вентилятор при необходимости сильного обогрева/охлаждения", + "on_command_text": "Для устройств типа `select` или `climate` необходимо настроить команды." + } + }, + "tpi": { + "title": "TPI - {name}", + "description": "Атрибуты Time Proportional Integral", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "tpi_threshold_low": "Нижний порог TPI", + "tpi_threshold_high": "Верхний порог TPI", + "use_tpi_central_config": "Использовать центральную конфигурацию TPI", + "minimal_activation_delay": "Минимальная задержка активации", + "minimal_deactivation_delay": "Минимальная задержка деактивации" + }, + "data_description": { + "tpi_coef_int": "Коэффициент для дельты внутренней температуры", + "tpi_coef_ext": "Коэффициент для дельты внешней температуры", + "tpi_threshold_low": "Порог в °, ниже которого алгоритм TPI будет включен. 0 означает отсутствие порога", + "tpi_threshold_high": "Порог в °, выше которого алгоритм TPI будет выключен. 0 означает отсутствие порога", + "use_tpi_central_config": "Отметьте для использования центральной конфигурации TPI. Снимите для использования специфичной конфигурации TPI для этого VTherm", + "minimal_activation_delay": "Задержка в секундах, ниже которой оборудование не будет активировано", + "minimal_deactivation_delay": "Задержка в секундах, ниже которой оборудование будет оставаться активным" + } + }, + "presets": { + "title": "Предустановки - {name}", + "description": "Отметьте, если термостат будет использовать центральные предустановки. Снимите и термостат будет иметь собственные сущности предустановок", + "data": { + "use_presets_central_config": "Использовать центральную конфигурацию предустановок" + } + }, + "window": { + "title": "Окна - {name}", + "description": "Управление открытыми окнами.\nВы также можете настроить автоматическое обнаружение открытых окон на основе снижения температуры", + "data": { + "window_sensor_entity_id": "ID датчика окна", + "window_delay": "Задержка датчика 'включено' (секунды)", + "window_off_delay": "Задержка датчика 'выключено' (секунды)", + "window_auto_open_threshold": "Порог снижения температуры для автоматического обнаружения открытого окна (в °/час)", + "window_auto_close_threshold": "Порог повышения температуры для окончания автоматического обнаружения (в °/час)", + "window_auto_max_duration": "Максимальная длительность автоматического обнаружения открытого окна (в мин)", + "use_window_central_config": "Использовать центральную конфигурацию окон", + "window_action": "Действие" + }, + "data_description": { + "window_sensor_entity_id": "Оставьте пустым, если датчик окна не используется и для использования автоматического обнаружения", + "window_delay": "Задержка в секундах перед учетом обнаружения датчика 'включено'", + "window_off_delay": "Задержка в секундах перед учетом обнаружения датчика 'выключено'. Оставьте пустым для использования того же значения, что и для задержки включения", + "window_auto_close_threshold": "Рекомендуемое значение: 0. Оставьте пустым, если автоматическое обнаружение не используется", + "window_auto_max_duration": "Рекомендуемое значение: 60 (один час). Оставьте пустым, если автоматическое обнаружение не используется", + "use_window_central_config": "Отметьте для использования центральной конфигурации окон. Снимите для использования специфичной конфигурации окон для этого VTherm", + "window_action": "Действие при обнаружении открытого окна" + } + }, + "motion": { + "title": "Движение - {name}", + "description": "Управление движением. Предустановка может автоматически переключаться в зависимости от обнаружения движения\nmotion_preset и no_motion_preset должны быть установлены в соответствующие имена предустановок", + "data": { + "motion_sensor_entity_id": "ID датчика движения", + "motion_delay": "Задержка активации", + "motion_off_delay": "Задержка деактивации", + "motion_preset": "Предустановка при движении", + "no_motion_preset": "Предустановка без движения", + "use_motion_central_config": "Использовать центральную конфигурацию движения" + }, + "data_description": { + "motion_sensor_entity_id": "ID сущности датчика движения", + "motion_delay": "Задержка активации движения (секунды)", + "motion_off_delay": "Задержка деактивации движения (секунды)", + "motion_preset": "Предустановка для использования при обнаружении движения", + "no_motion_preset": "Предустановка для использования при отсутствии движения", + "use_motion_central_config": "Отметьте для использования центральной конфигурации движения. Снимите для использования специфичной конфигурации движения для этого VTherm" + } + }, + "power": { + "title": "Питание - {name}", + "description": "Атрибуты управления питанием.\nУказывает датчики мощности и максимальной мощности вашего дома.\nУкажите потребляемую мощность обогревателя при включении.\nВсе датчики и мощность устройства должны использовать одинаковые единицы измерения (кВт или Вт)", + "data": { + "power_sensor_entity_id": "Мощность", + "max_power_sensor_entity_id": "Максимальная мощность", + "power_temp": "Температура снижения мощности", + "use_power_central_config": "Использовать центральную конфигурацию питания" + }, + "data_description": { + "power_sensor_entity_id": "ID датчика мощности", + "max_power_sensor_entity_id": "ID датчика максимальной мощности", + "power_temp": "Температура для снижения мощности", + "use_power_central_config": "Отметьте для использования центральной конфигурации питания. Снимите для использования специфичной конфигурации питания для этого VTherm" + } + }, + "presence": { + "title": "Присутствие - {name}", + "description": "Атрибуты управления присутствием.\nУказывает датчик присутствия в вашем доме (true если кто-то присутствует) и соответствующую настройку температурной предустановки", + "data": { + "presence_sensor_entity_id": "Датчик присутствия", + "use_presence_central_config": "Использовать центральную конфигурацию температуры присутствия. Снимите для использования специфичных температурных сущностей" + }, + "data_description": { + "presence_sensor_entity_id": "ID датчика присутствия" + } + }, + "advanced": { + "title": "Расширенные - {name}", + "description": "Конфигурация расширенных параметров. Оставьте значения по умолчанию, если не уверены в своих действиях.\nЭти параметры могут привести к очень плохому контролю температуры или плохому регулированию мощности", + "data": { + "minimal_activation_delay": "Минимальная задержка активации", + "minimal_deactivation_delay": "Минимальная задержка деактивации", + "safety_delay_min": "Защитная задержка (в минутах)", + "safety_min_on_percent": "Минимальный процент мощности для включения защитного режима", + "safety_default_on_percent": "Процент мощности для использования в защитном режиме", + "repair_incorrect_state": "Исправлять неправильное состояние", + "use_advanced_central_config": "Использовать центральную расширенную конфигурацию" + }, + "data_description": { + "minimal_activation_delay": "Задержка в секундах, ниже которой оборудование не будет активировано", + "minimal_deactivation_delay": "Задержка в секундах, ниже которой оборудование будет оставаться активным", + "safety_delay_min": "Максимально допустимая задержка в минутах между двумя измерениями температуры. Выше этой задержки термостат перейдет в защитный режим выключения", + "safety_min_on_percent": "Минимальное значение процента нагрева для активации защитной предустановки. Ниже этого значения процента мощности термостат не перейдет в защитную предустановку", + "safety_default_on_percent": "Значение процента мощности нагрева по умолчанию в защитной предустановке. Установите 0 для выключения обогревателя в защитной предустановке", + "repair_incorrect_state": "Автоматически исправлять неправильное состояние базовых сущностей при обнаружении. Функция повторно отправляет желаемую команду, если состояние базового устройства не совпадает с ожидаемым состоянием.", + "use_advanced_central_config": "Отметьте для использования центральной расширенной конфигурации. Снимите для использования специфичной расширенной конфигурации для этого VTherm" + } + }, + "central_boiler": { + "title": "Управление центральным котлом - {name}", + "description": "Введите сервисы для вызова включения/выключения центрального котла. Вызываемый сервис должен быть отформатирован следующим образом: `entity_id/service_name[/attribute:value]` (/attribute:value опционально)\nНапример:\n- для включения переключателя: `switch.controle_chaudiere/switch.turn_on`\n- для выключения переключателя: `switch.controle_chaudiere/switch.turn_off`\n- для программирования котла на 25° и принудительного включения: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- для отправки 10° котлу и принудительного выключения: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`", + "data": { + "central_boiler_activation_service": "Команда включения", + "central_boiler_deactivation_service": "Команда выключения", + "central_boiler_activation_delay_sec": "Задержка включения (в секундах)", + "keep_alive_boiler_delay_sec": "Задержка keep-alive (секунды)" + }, + "data_description": { + "central_boiler_activation_service": "Команда включения центрального котла в формате entity_id/service_name[/attribut:valeur]", + "central_boiler_deactivation_service": "Команда выключения центрального котла в формате entity_id/service_name[/attribut:valeur]", + "central_boiler_activation_delay_sec": "Задержка включения (в секундах) перед включением центрального котла после того, как VTherm запросил включение", + "keep_alive_boiler_delay_sec": "Задержка в секундах для периодической повторной отправки команды котла, если keep-alive включен. Установите 0, чтобы отключить keep-alive." + } + }, + "valve_regulation": { + "title": "Саморегулирование с клапаном - {name}", + "description": "Конфигурация для саморегулирования с прямым управлением клапаном", + "data": { + "offset_calibration_entity_ids": "Сущности калибровки смещения", + "opening_degree_entity_ids": "Сущности степени открытия", + "closing_degree_entity_ids": "Сущности степени закрытия", + "proportional_function": "Алгоритм", + "opening_threshold_degree": "Пороговое значение открытия", + "min_opening_degrees": "Минимальные степени открытия", + "max_opening_degrees": "Максимальная степень открытия", + "max_closing_degree": "Максимальная степень закрытия" + }, + "data_description": { + "offset_calibration_entity_ids": "Список сущностей 'калибровки смещения'. Установите, если ваш TRV имеет такую сущность для лучшего регулирования. Должна быть одна для каждой управляющей климатической сущности", + "opening_degree_entity_ids": "Список сущностей 'степени открытия'. Должна быть одна для каждой управляющей климатической сущности", + "closing_degree_entity_ids": "Список сущностей 'степени закрытия'. Установите, если ваш TRV имеет такую сущность для лучшего регулирования. Должна быть одна для каждой управляющей климатической сущности", + "proportional_function": "Алгоритм для использования (другие алгоритмы можно установить как {plugins_link})", + "opening_threshold_degree": "Значение открытия клапана, ниже которого клапан должен считаться закрытым (и тогда будет применено 'max_closing_degree')", + "min_opening_degrees": "Минимальное значение степени открытия для каждого управляющего устройства, через запятую. По умолчанию 0. Пример: 20, 25, 30", + "max_opening_degrees": "Максимальное значение степени открытия для каждого базового устройства, через запятую. По умолчанию 100. Клапан никогда не откроется больше этого значения. Пример: 80, 85, 90", + "max_closing_degree": "Максимальное значение степени закрытия. Клапан никогда не закроется больше этого значения. Установите на 100, чтобы полностью закрыть клапан" + } + }, + "sync_device_internal_temp": { + "title": "Synchronize device internal temperature", + "description": "Configuration to synchronize the internal temperature of the underlying devices with selected entities", + "data": { + "sync_with_calibration": "Apply offset calibration", + "sync_entity_ids": "Entities id used for synchronization" + }, + "data_description": { + "sync_with_calibration": "Check to apply the offset calibration when synchronizing the internal temperature. Uncheck to directly copy the temperature from the selected entities", + "sync_entity_ids": "The list of the entities used to synchronize the internal temperature of the underlying devices. There should be one per underlying device. Should be a calibration entity if checkbox is selected or a temperature entity otherwise. Both are `number` entities." + } + }, + "heating_failure_detection": { + "title": "Обнаружение сбоев отопления", + "description": "Настройка функции обнаружения сбоев отопления. Обнаруживает аномалии, когда ожидается нагрев, но температура не повышается, или когда отопление выключено, но температура продолжает расти.", + "data": { + "use_heating_failure_detection_feature": "Включить обнаружение сбоев отопления", + "use_heating_failure_detection_central_config": "Использовать центральную конфигурацию обнаружения сбоев", + "heating_failure_threshold": "Порог сбоя отопления", + "cooling_failure_threshold": "Порог сбоя охлаждения", + "heating_failure_detection_delay": "Задержка обнаружения (минуты)", + "temperature_change_tolerance": "Допуск изменения температуры (°C)", + "failure_detection_enable_template": "Шаблон включения обнаружения" + }, + "data_description": { + "use_heating_failure_detection_feature": "Включить обнаружение аномалий отопления для VTherm с TPI", + "use_heating_failure_detection_central_config": "Отметьте для использования центральной конфигурации обнаружения сбоев. Снимите отметку для использования специфических параметров для этого VTherm", + "heating_failure_threshold": "Пороговый процент включения, выше которого отопление должно вызывать повышение температуры (0.9 = 90%)", + "cooling_failure_threshold": "Пороговый процент включения, ниже которого температура не должна повышаться (0.0 = 0%)", + "heating_failure_detection_delay": "Время в минутах ожидания перед проверкой, изменилась ли температура как ожидалось", + "temperature_change_tolerance": "Минимальное изменение температуры в градусах, считающееся значительным. Меньшие изменения игнорируются для фильтрации шума датчика (по умолчанию: 0.5°C)", + "failure_detection_enable_template": "Необязательный шаблон Jinja2, который должен вернуть True для включения обнаружения. Полезно для временного отключения обнаружения при активном внешнем источнике тепла (например, `is_state('binary_sensor.wood_stove', 'off')` в двойных фигурных скобках). Если пусто, обнаружение всегда включено." + } + } + }, + "error": { + "unknown": "Неожиданная ошибка", + "unknown_entity": "Неизвестный ID сущности", + "window_open_detection_method": "Должен использоваться только один метод обнаружения открытых окон. Используйте либо датчик окна, либо автоматическое обнаружение через температурный порог, но не оба одновременно", + "no_central_config": "Вы не можете выбрать 'использовать центральную конфигурацию', потому что центральная конфигурация не найдена. Вам нужно создать универсальный термостат типа 'Центральная конфигурация' для его использования.", + "service_configuration_format": "Неверный формат конфигурации сервиса", + "valve_regulation_nb_entities_incorrect": "Количество сущностей клапана для регулирования клапана должно быть равно количеству управляющих устройств", + "sync_device_internal_temp_nb_entities_incorrect": "Количество сущностей для синхронизации внутренней температуры устройства должно быть равно количеству управляющих устройств", + "min_opening_degrees_format": "Ожидается список положительных целых чисел, разделенных запятыми. Пример: 20, 25, 30", + "max_opening_degrees_format": "Ожидается список положительных целых чисел от 0 до 100, разделенных запятыми. Пример: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Для каждого базового устройства max_opening_degrees должен быть строго больше min_opening_degrees. Проверьте вашу конфигурацию.", + "vswitch_configuration_incorrect": "Неверная конфигурация настройки команды. Она обязательна для базовых объектов, которые не являются переключателями, и формат должен быть service_name[/attribute:value]. Дополнительная информация доступна в README" + }, + "abort": { + "already_configured": "Устройство уже настроено" + } + }, + "selector": { + "thermostat_type": { + "options": { + "thermostat_central_config": "Центральная конфигурация", + "thermostat_over_switch": "Термостат на основе переключателя", + "thermostat_over_climate": "Термостат на основе климата", + "thermostat_over_valve": "Термостат на основе клапана" + } + }, + "auto_regulation_mode": { + "options": { + "auto_regulation_slow": "Медленный", + "auto_regulation_strong": "Сильный", + "auto_regulation_medium": "Средний", + "auto_regulation_light": "Легкий", + "auto_regulation_expert": "Экспертный", + "auto_regulation_none": "Без саморегулирования", + "auto_regulation_valve": "Прямое управление клапаном" + } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "Без авто вентилятора", + "auto_fan_low": "Низкий", + "auto_fan_medium": "Средний", + "auto_fan_high": "Высокий", + "auto_fan_turbo": "Турбо" + } + }, + "window_action": { + "options": { + "window_turn_off": "Выключить", + "window_fan_only": "Только вентилятор", + "window_frost_temp": "Защита от замерзания", + "window_eco_temp": "Эко" + } + }, + "presets": { + "options": { + "frost": "Защита от замерзания", + "eco": "Эко", + "comfort": "Комфорт", + "boost": "Интенсивный" + } + }, + "auto_start_stop": { + "options": { + "auto_start_stop_none": "Без авто старта/остановки", + "auto_start_stop_very_slow": "Очень медленное обнаружение", + "auto_start_stop_slow": "Медленное обнаружение", + "auto_start_stop_medium": "Среднее обнаружение", + "auto_start_stop_fast": "Быстрое обнаружение" + } + } + }, + "entity": { + "climate": { + "versatile_thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "power": "Снижение мощности", + "safety": "Безопасность", + "none": "Ручной", + "frost": "Защита от замерзания" + } + } + } + } + }, + "number": { + "frost_temp": { + "name": "Защита от замерзания" + }, + "eco_temp": { + "name": "Эко" + }, + "comfort_temp": { + "name": "Комфорт" + }, + "boost_temp": { + "name": "Интенсивный" + }, + "frost_ac_temp": { + "name": "Защита от замерзания AC" + }, + "eco_ac_temp": { + "name": "Эко AC" + }, + "comfort_ac_temp": { + "name": "Комфорт AC" + }, + "boost_ac_temp": { + "name": "Интенсивный AC" + }, + "frost_away_temp": { + "name": "Защита от замерзания (отсутствие)" + }, + "eco_away_temp": { + "name": "Эко (отсутствие)" + }, + "comfort_away_temp": { + "name": "Комфорт (отсутствие)" + }, + "boost_away_temp": { + "name": "Интенсивный (отсутствие)" + }, + "eco_ac_away_temp": { + "name": "Эко AC (отсутствие)" + }, + "comfort_ac_away_temp": { + "name": "Комфорт AC (отсутствие)" + }, + "boost_ac_away_temp": { + "name": "Интенсивный AC (отсутствие)" + } + } + } +} \ No newline at end of file diff --git a/custom_components/versatile_thermostat/translations/sk.json b/custom_components/versatile_thermostat/translations/sk.json new file mode 100644 index 0000000..58f8430 --- /dev/null +++ b/custom_components/versatile_thermostat/translations/sk.json @@ -0,0 +1,766 @@ +{ + "title": "Všestranná konfigurácia termostatu", + "config": { + "flow_title": "Všestranná konfigurácia termostatu", + "step": { + "user": { + "title": "Typ všestranného termostatu", + "data": { + "thermostat_type": "Typ termostatu" + }, + "data_description": { + "thermostat_type": "Len jeden centrálny typ konfigurácie je možný" + } + }, + "menu": { + "title": "Menu", + "description": "Nakonfigurujte si termostat. Po zadaní všetkých požadovaných parametrov budete môcť dokončiť konfiguráciu.", + "menu_options": { + "main": "Hlavné atribúty", + "central_boiler": "Centrálny kotol", + "type": "Podklady", + "tpi": "TPI parametre", + "features": "Vlastnosti", + "presets": "Predvoľby", + "window": "Detekcia okien", + "motion": "Detekcia pohybu", + "power": "Správa napájania", + "presence": "Detekcia prítomnosti", + "advanced": "Pokročilé parametre", + "auto_start_stop": "Automatické spustenie a zastavenie", + "valve_regulation": "Konfigurácia regulácie ventilu", + "sync_device_internal_temp": "Synchronizácia teploty zariadenia", + "lock": "Správa zámku", + "finalize": "Všetko hotové", + "configuration_not_complete": "Konfigurácia nie je dokončená", + "heating_failure_detection": "Detekcia porúch vykurovania" + } + }, + "lock": { + "title": "Správa zámku", + "description": "Funkcia zámku bráni zmenám v konfigurácii termostatu z používateľského rozhrania alebo automatizácií, zatiaľ čo termostat zostáva funkčný.", + "data": { + "use_lock_central_config": "Použiť centrálnu konfiguráciu zámku", + "lock_code": "PIN kód na odomknutie termostatu (4 číslice)", + "lock_users": "Zámok zabráni príkazom od používateľov", + "lock_automations": "Zámok zabráni príkazom z automatizácií a integrácií (napr. plánovača)" + }, + "data_description": { + "use_lock_central_config": "Začiarknite, ak chcete použiť centrálnu konfiguráciu zámku. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu zámku pre tento VTherm", + "lock_code": "PIN kód na odomknutie termostatu (4 číslice)" + } + }, + "main": { + "title": "Pridajte nový všestranný termostat", + "description": "Hlavné povinné atribúty", + "data": { + "name": "Názov", + "thermostat_type": "Termostat typ", + "temperature_sensor_entity_id": "ID entity snímača teploty", + "last_seen_temperature_sensor_entity_id": "Dátum posledného zobrazenia izbovej teploty", + "external_temperature_sensor_entity_id": "ID entity externého snímača teploty", + "cycle_min": "Trvanie cyklu (minúty)", + "temp_min": "Minimálna povolená teplota", + "temp_max": "Maximálna povolená teplota", + "step_temperature": "Krok teploty", + "device_power": "Napájanie zariadenia", + "use_central_mode": "Povoliť ovládanie centrálnou entitou (potrebná centrálna konfigurácia)", + "use_main_central_config": "Použite dodatočnú centrálnu hlavnú konfiguráciu. Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu (vonkajšia teplota, min, max, krok, ...).", + "used_by_controls_central_boiler": "Používa sa centrálnym kotlom. Skontrolujte, či má mať tento VTherm ovládanie na centrálnom kotli" + }, + "data_description": { + "cycle_min": "Minimálna dĺžka jedného cyklu vykurovania/chladenia v minútach, aby sa zabránilo príliš častému prepínaniu", + "temp_min": "Minimálna povolená teplota pre termostat", + "temp_max": "Maximálna povolená teplota pre termostat", + "step_temperature": "Krok teploty na nastavenie cieľovej teploty", + "temperature_sensor_entity_id": "ID entity snímača izbovej teploty", + "last_seen_temperature_sensor_entity_id": "Naposledy videný snímač izbovej teploty ID entity. Mal by to byť snímač dátumu a času", + "external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia" + } + }, + "features": { + "title": "Vlastnosti", + "description": "Vlastnosti termostatu", + "data": { + "use_window_feature": "Použite detekciu okien", + "use_motion_feature": "Použite detekciu pohybu", + "use_power_feature": "Použite správu napájania", + "use_presence_feature": "Použite detekciu prítomnosti", + "use_central_boiler_feature": "Použite centrálny kotol. Začiarknutím tohto políčka pridáte ovládanie do centrálneho kotla. Po zaškrtnutí tohto políčka budete musieť nakonfigurovať VTherm, ktorý bude mať ovládanie centrálneho kotla, aby sa prejavilo. Ak jeden VTherm vyžaduje ohrev, kotol sa zapne. Ak žiadny VTherm nevyžaduje ohrev, kotol sa vypne. Príkazy na zapnutie/vypnutie centrálneho kotla sú uvedené na príslušnej konfiguračnej stránke", + "use_heating_failure_detection_feature": "Použiť detekciu porúch vykurovania" + } + }, + "type": { + "title": "Prepojené entity", + "description": "Atribúty prepojených entít", + "data": { + "heater_entity_id": "1. spínač ohrievača", + "heater_entity2_id": "2. spínač ohrievača", + "heater_entity3_id": "3. spínač ohrievača", + "heater_entity4_id": "4. spínač ohrievača", + "heater_keep_alive": "Prepnite interval udržiavania v sekundách", + "proportional_function": "Algoritmus", + "climate_entity_id": "1. základná klíma", + "climate_entity2_id": "2. základná klíma", + "climate_entity3_id": "3. základná klíma", + "climate_entity4_id": "4. základná klíma", + "ac_mode": "AC režim", + "sync_device_internal_temp": "Synchronizácia vnútornej teploty zariadenia", + "valve_entity_id": "1. ventil číslo", + "valve_entity2_id": "2. ventil číslo", + "valve_entity3_id": "3. ventil číslo", + "valve_entity4_id": "4. ventil číslo", + "auto_regulation_mode": "Samoregulácia", + "auto_regulation_dtemp": "Regulačný prah", + "auto_regulation_periode_min": "Regulačné minimálne obdobie", + "auto_regulation_use_device_temp": "Použite vnútornú teplotu podkladu", + "inverse_switch_command": "Inverzný prepínací príkaz", + "auto_fan_mode": "Režim automatického ventilátora" + }, + "data_description": { + "heater_entity_id": "ID entity povinného ohrievača", + "heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne", + "heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne", + "heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne", + "heater_keep_alive": "Voliteľný interval obnovy stavu spínača ohrievača. Ak to nie je potrebné, nechajte prázdne.", + "proportional_function": "Algoritmus, ktorý sa má použiť (ďalšie algoritmy je možné nainštalovať ako {plugins_link})", + "climate_entity_id": "ID základnej klimatickej entity", + "climate_entity2_id": "2. základné identifikačné číslo klimatickej entity", + "climate_entity3_id": "3. základné identifikačné číslo klimatickej entity", + "climate_entity4_id": "4. základné identifikačné číslo klimatickej entity", + "ac_mode": "Použite režim klimatizácie (AC)", + "sync_device_internal_temp": "Synchronizácia vnútornej teploty zariadenia", + "valve_entity_id": "1. ventil číslo entity id", + "valve_entity2_id": "2. ventil číslo entity id", + "valve_entity3_id": "3. ventil číslo entity id", + "valve_entity4_id": "4. ventil číslo entity id", + "auto_regulation_mode": "Automatické nastavenie cieľovej teploty", + "auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle", + "auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov", + "auto_regulation_use_device_temp": "Na urýchlenie samoregulácie použite prípadný vnútorný snímač teploty podkladu (vo väčšine prípadov nie je potrebné)", + "inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať", + "auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie" + } + }, + "tpi": { + "title": "TPI", + "description": "Časovo proporcionálne integrálne atribúty", + "data": { + "tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu", + "tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty", + "use_tpi_central_config": "Použite centrálnu konfiguráciu TPI" + }, + "data_description": { + "tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu", + "tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty", + "use_tpi_central_config": "Začiarknite, ak chcete použiť centrálnu konfiguráciu TPI. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu TPI pre tento VTherm" + } + }, + "presets": { + "title": "Predvoľby", + "description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)", + "data": { + "use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb" + } + }, + "window": { + "title": "Správa okien", + "description": "Otvoriť správu okien.\nAk sa príslušné entity_id nepoužíva, ponechajte prázdne\nMôžete tiež nakonfigurovať automatickú detekciu otvoreného okna na základe poklesu teploty", + "data": { + "window_sensor_entity_id": "ID entity snímača okna", + "window_delay": "Oneskorenie snímača okna (sekundy)", + "window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)", + "window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)", + "window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)", + "use_window_central_config": "Použite centrálnu konfiguráciu okna", + "window_action": "Akcia" + }, + "data_description": { + "window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor", + "window_delay": "Zohľadňuje sa oneskorenie v sekundách pred detekciou snímača", + "window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne", + "window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne", + "window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne", + "use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm", + "window_action": "Akcia, ktorá sa má vykonať, ak sa okno zistí ako otvorené" + } + }, + "motion": { + "title": "Riadenie pohybu", + "description": "Správa snímača pohybu. Predvoľba sa môže automaticky prepínať v závislosti od detekcie pohybu\nAk sa nepoužíva, ponechajte zodpovedajúce entity_id prázdne.\nmotion_preset a no_motion_preset by mali byť nastavené na zodpovedajúci názov predvoľby", + "data": { + "motion_sensor_entity_id": "ID entity snímača pohybu", + "motion_delay": "Oneskorenie aktivácie", + "motion_off_delay": "Oneskorenie deaktivácie", + "motion_preset": "Prednastavený pohyb", + "no_motion_preset": "Žiadna predvoľba pohybu", + "use_motion_central_config": "Použite centrálnu konfiguráciu pohybu" + }, + "data_description": { + "motion_sensor_entity_id": "ID entity snímača pohybu", + "motion_delay": "Oneskorenie aktivácie pohybu (sekundy)", + "motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)", + "motion_preset": "Prednastavené na použitie pri detekcii pohybu", + "no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb", + "use_motion_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho pohybu. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu pohybu pre tento VTherm" + } + }, + "power": { + "title": "Správa napájania", + "description": "Atribúty správy napájania.\nPoskytuje senzor výkonu a maximálneho výkonu vášho domova.\nPotom zadajte spotrebu energie ohrievača, keď je zapnutý.\nVšetky senzory a výkon zariadenia by mali mať rovnakú jednotku (kW alebo W).\nPonechajte zodpovedajúce entity_id prázdne ak sa nepoužíva.", + "data": { + "power_sensor_entity_id": "ID entity snímača výkonu", + "max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu", + "power_temp": "Teplota pre zníženie výkonu", + "use_power_central_config": "Použite centrálnu konfiguráciu napájania" + }, + "data_description": { + "power_sensor_entity_id": "ID entity snímača výkonu", + "max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu", + "power_temp": "Teplota pre zníženie výkonu", + "use_power_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho napájania. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu napájania pre tento VTherm" + } + }, + "presence": { + "title": "Riadenie prítomnosti", + "description": "Atribúty správy prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, ak je niekto prítomný).\nPotom zadajte buď predvoľbu, ktorá sa má použiť, keď je senzor prítomnosti nepravdivý, alebo posun teploty, ktorý sa má použiť.\nAk je zadaná predvoľba, posun sa nepoužije.\nAk sa nepoužije, ponechajte zodpovedajúce entity_id prázdne.", + "data": { + "presence_sensor_entity_id": "Senzora prítomnosti", + "use_presence_central_config": "Použite konfiguráciu centrálnej prítomnosti teploty. Ak chcete použiť špecifické teplotné entity, zrušte výber" + }, + "data_description": { + "presence_sensor_entity_id": "ID entity senzora prítomnosti" + } + }, + "advanced": { + "title": "Pokročilé parametre", + "description": "Konfigurácia pokročilých parametrov. Ak neviete, čo robíte, ponechajte predvolené hodnoty.\nTento parameter môže viesť k veľmi zlej regulácii teploty alebo výkonu.", + "data": { + "minimal_activation_delay": "Minimálne oneskorenie aktivácie", + "safety_delay_min": "Bezpečnostné oneskorenie (v minútach)", + "safety_min_on_percent": "Minimálne percento výkonu na aktiváciu bezpečnostného režimu", + "safety_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime", + "repair_incorrect_state": "Opraviť nesprávny stav", + "use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu" + }, + "data_description": { + "minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje", + "safety_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu", + "safety_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia", + "safety_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave", + "repair_incorrect_state": "Automaticky opraviť nesprávny stav základných zariadení pri detekcií. Funkcia znovu pošle požadovaný príkaz, ak stav základného zariadenia nezodpovedá očakávanému stavu.", + "use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm" + } + }, + "sync_device_internal_temp": { + "title": "Synchronize device internal temperature", + "description": "Configuration to synchronize the internal temperature of the underlying devices with selected entities", + "data": { + "sync_with_calibration": "Apply offset calibration", + "sync_entity_ids": "Entities id used for synchronization" + }, + "data_description": { + "sync_with_calibration": "Check to apply the offset calibration when synchronizing the internal temperature. Uncheck to directly copy the temperature from the selected entities", + "sync_entity_ids": "The list of the entities used to synchronize the internal temperature of the underlying devices. There should be one per underlying device. Should be a calibration entity if checkbox is selected or a temperature entity otherwise. Both are `number` entities." + } + }, + "valve_regulation": { + "title": "Samoregulácia s ventilon", + "description": "Konfigurácia samoregulácie s priamym ovládaním ventilu", + "data": { + "offset_calibration_entity_ids": "Entity kalibrácie offsetu", + "opening_degree_entity_ids": "Entity stupňa otvorenia", + "closing_degree_entity_ids": "Entity stupňa zatvorenia", + "proportional_function": "Algoritmus", + "opening_threshold_degree": "Prah otvorenia", + "min_opening_degrees": "Minimálne stupne otvorenia", + "max_opening_degrees": "Maximálny stupeň otvorenia", + "max_closing_degree": "Maximálny stupeň zatvorenia" + }, + "data_description": { + "offset_calibration_entity_ids": "Zoznam entít 'kalibrácie offsetu'. Nastavte, ak má váš TRV entitu pre lepšiu reguláciu. Mala by byť jedna na každú základnú klimatickú entitu", + "opening_degree_entity_ids": "Zoznam entít 'stupňa otvorenia'. Mala by byť jedna na každú základnú klimatickú entitu", + "closing_degree_entity_ids": "Zoznam entít 'stupňa zatvorenia'. Nastavte, ak má váš TRV entitu pre lepšiu reguláciu. Mala by byť jedna na každú základnú klimatickú entitu", + "proportional_function": "Algoritmus, ktorý sa má použiť (ďalšie algoritmy je možné nainštalovať ako {plugins_link})", + "opening_threshold_degree": "Hodnota otvorenia ventilu, pod ktorou by mal byť ventil považovaný za zatvorený (a potom bude platný 'max_closing_degree')", + "min_opening_degrees": "Minimálna hodnota stupňa otvorenia pre každé základné zariadenie, oddelená čiarkami. Predvolená hodnota je 0. Príklad: 20, 25, 30", + "max_opening_degrees": "Maximálna hodnota stupňa otvorenia pre každé základné zariadenie, oddelené čiarkami. Predvolená hodnota je 100. Ventil sa nikdy neotvorí viac ako táto hodnota. Príklad: 80, 85, 90", + "max_closing_degree": "Maximálna hodnota stupňa zatvorenia. Ventil sa nikdy nezatvorí viac ako táto hodnota. Nastavte na 100 pre úplné zatvorenie ventilu" + } + }, + "heating_failure_detection": { + "title": "Detekcia porúch vykurovania", + "description": "Konfigurácia funkcie detekcie porúch vykurovania. Detekuje anomálie, keď sa očakáva vykurovanie, ale teplota nestúpa, alebo keď je vykurovanie vypnuté, ale teplota stále stúpa.", + "data": { + "use_heating_failure_detection_feature": "Povoliť detekciu porúch vykurovania", + "use_heating_failure_detection_central_config": "Použiť centrálnu konfiguráciu detekcie porúch", + "heating_failure_threshold": "Prah poruchy vykurovania", + "cooling_failure_threshold": "Prah poruchy chladenia", + "heating_failure_detection_delay": "Oneskorenie detekcie (minúty)", + "temperature_change_tolerance": "Tolerancia zmeny teploty (°C)", + "failure_detection_enable_template": "Šablóna povolenia detekcie" + }, + "data_description": { + "use_heating_failure_detection_feature": "Povoliť detekciu anomálií vykurovania pre VTherm s TPI", + "use_heating_failure_detection_central_config": "Zaškrtnite pre použitie centrálnej konfigurácie detekcie porúch. Odškrtnite pre použitie špecifických parametrov pre tento VTherm", + "heating_failure_threshold": "Percentuálny prah zapnutia, nad ktorým by vykurovanie malo spôsobiť zvýšenie teploty (0.9 = 90%)", + "cooling_failure_threshold": "Percentuálny prah zapnutia, pod ktorým by teplota nemala stúpať (0.0 = 0%)", + "heating_failure_detection_delay": "Čas v minútach na čakanie pred kontrolou, či sa teplota zmenila podľa očakávania", + "temperature_change_tolerance": "Minimálna zmena teploty v stupňoch považovaná za významnú. Menšie zmeny sú ignorované na filtrovanie šumu snímača (predvolené: 0.5°C)", + "failure_detection_enable_template": "Voliteľná šablóna Jinja2, ktorá musí vrátiť True na povolenie detekcie. Užitočné na dočasné vypnutie detekcie, keď je aktívny externý zdroj tepla (napr. `is_state('binary_sensor.pec_na_drevo', 'off')` v dvojitých zložených zátvorkách). Ak je prázdne, detekcia je vždy povolená." + } + } + }, + "error": { + "unknown": "Neočakávaná chyba", + "unknown_entity": "Neznáme ID entity", + "window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje", + "no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“.", + "service_configuration_format": "Formát konfigurácie služby je nesprávny", + "valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings", + "sync_device_internal_temp_nb_entities_incorrect": "The number of entities for synchronize device internal temperature should be equal to the number of underlyings", + "min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30", + "max_opening_degrees_format": "Očakáva sa zoznam kladných celých čísel medzi 0 a 100 oddelených čiarkou. Príklad: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Pre každé základné zariadenie musí byť max_opening_degrees striktne väčší ako min_opening_degrees. Skontrolujte prosím svoju konfiguráciu.", + "vswitch_configuration_incorrect": "The command customization configuration is incorrect. It is required for underlying entities that are not switches, and the format must be service_name[/attribute:value]. More information is available in the README" + }, + "abort": { + "already_configured": "Zariadenie je už nakonfigurované" + } + }, + "options": { + "flow_title": "Všestranná konfigurácia termostatu", + "step": { + "user": { + "title": "Typ - {name}", + "data": { + "thermostat_type": "Typ termostatu" + }, + "data_description": { + "thermostat_type": "Je možný len jeden centrálny typ konfigurácie" + } + }, + "menu": { + "title": "Menu", + "description": "Nakonfigurujte si termostat. Po zadaní všetkých požadovaných parametrov budete môcť dokončiť konfiguráciu.", + "menu_options": { + "main": "Hlavné atribúty", + "central_boiler": "Centrálny kotol", + "type": "Podklady", + "tpi": "TPI parametre", + "features": "Vlastnosti", + "presets": "Predvoľby", + "window": "Detekcia okien", + "motion": "Detekcia pohybu", + "power": "Správa napájania", + "presence": "Detekcia prítomnosti", + "advanced": "Pokročilé parametre", + "auto_start_stop": "Automatické spustenie a zastavenie", + "valve_regulation": "Konfigurácia regulácie ventilu", + "sync_device_internal_temp": "Synchronizácia teploty zariadenia", + "lock": "Správa zámku", + "finalize": "Všetko hotové", + "configuration_not_complete": "Konfigurácia nie je dokončená", + "heating_failure_detection": "Detekcia porúch vykurovania" + } + }, + "lock": { + "title": "Správa zámku - {name}", + "description": "Funkcia zámku zabraňuje zmenám termostatu z používateľského rozhrania alebo automatizácií, pričom termostat zostáva funkčný.", + "data": { + "use_lock_central_config": "Použiť centrálnu konfiguráciu zámku", + "lock_code": "PIN kód na odomknutie termostatu (4 číslice)", + "lock_users": "Zámok zabráni príkazom od používateľov", + "lock_automations": "Zámok zabráni príkazom z automatizácií a integrácií (napr. plánovača)" + }, + "data_description": { + "use_lock_central_config": "Začiarknite, ak chcete použiť centrálnu konfiguráciu zámku. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu zámku pre tento VTherm", + "lock_code": "PIN kód na odomknutie termostatu (4 číslice)" + } + }, + "main": { + "title": "Hlavný - {name}", + "description": "Hlavné povinné atribúty", + "data": { + "name": "Názov", + "thermostat_type": "Termostat typ", + "temperature_sensor_entity_id": "ID entity snímača teploty", + "last_seen_temperature_sensor_entity_id": "Dátum posledného zobrazenia izbovej teploty", + "external_temperature_sensor_entity_id": "ID entity externého snímača teploty", + "cycle_min": "Trvanie cyklu (minúty)", + "temp_min": "Minimálna povolená teplota", + "temp_max": "Maximálna povolená teplota", + "step_temperature": "Krok teploty", + "device_power": "Výkon zariadenia (kW)", + "use_central_mode": "Povoliť ovládanie centrálnou entitou (vyžaduje centrálnu konfiguráciu). Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode.", + "use_main_central_config": "Použite dodatočnú centrálnu hlavnú konfiguráciu. Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu (vonkajšia teplota, min, max, krok, ...).", + "used_by_controls_central_boiler": "Používa sa centrálnym kotlom. Skontrolujte, či má mať tento VTherm ovládanie na centrálnom kotli" + }, + "data_description": { + "cycle_min": "Minimálna dĺžka jedného cyklu vykurovania/chladenia v minútach, aby sa zabránilo príliš častému prepínaniu", + "temp_min": "Minimálna povolená teplota pre termostat", + "temp_max": "Maximálna povolená teplota pre termostat", + "step_temperature": "Krok teploty na nastavenie cieľovej teploty", + "temperature_sensor_entity_id": "ID entity snímača izbovej teploty", + "last_seen_temperature_sensor_entity_id": "Naposledy videný snímač izbovej teploty ID entity. Mal by to byť snímač dátumu a času", + "external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia" + } + }, + "features": { + "title": "Vlastnosti - {name}", + "description": "Vlastnosti termostatu", + "data": { + "use_window_feature": "Použite detekciu okien", + "use_motion_feature": "Použite detekciu pohybu", + "use_power_feature": "Použite správu napájania", + "use_presence_feature": "Použite detekciu prítomnosti", + "use_central_boiler_feature": "Použite centrálny kotol. Začiarknutím tohto políčka pridáte ovládanie do centrálneho kotla. Po zaškrtnutí tohto políčka budete musieť nakonfigurovať VTherm, ktorý bude mať ovládanie centrálneho kotla, aby sa prejavilo. Ak jeden VTherm vyžaduje ohrev, kotol sa zapne. Ak žiadny VTherm nevyžaduje ohrev, kotol sa vypne. Príkazy na zapnutie/vypnutie centrálneho kotla sú uvedené na príslušnej konfiguračnej stránke", + "use_heating_failure_detection_feature": "Použiť detekciu porúch vykurovania" + } + }, + "type": { + "title": "Prepojené entity - {name}", + "description": "Atribúty prepojených entít", + "data": { + "heater_entity_id": "Spínač ohrievača", + "heater_entity2_id": "2. spínač ohrievača", + "heater_entity3_id": "3. spínač ohrievača", + "heater_entity4_id": "4. spínač ohrievača", + "heater_keep_alive": "Prepnite interval udržiavania v sekundách", + "proportional_function": "Algoritmus", + "climate_entity_id": "Základná klíma", + "climate_entity2_id": "2. základná klíma", + "climate_entity3_id": "3. základná klíma", + "climate_entity4_id": "4. základná klíma", + "ac_mode": "AC režim", + "sync_device_internal_temp": "Synchronizácia vnútornej teploty zariadenia", + "valve_entity_id": "1. ventil číslo", + "valve_entity2_id": "2. ventil číslo", + "valve_entity3_id": "3. ventil číslo", + "valve_entity4_id": "4. ventil číslo", + "auto_regulation_mode": "Samoregulácia", + "auto_regulation_dtemp": "Regulačný prah", + "auto_regulation_periode_min": "Regulačné minimálne obdobie", + "auto_regulation_use_device_temp": "Použite vnútornú teplotu podkladu", + "inverse_switch_command": "Inverzný prepínací príkaz", + "auto_fan_mode": "Režim automatického ventilátora" + }, + "data_description": { + "heater_entity_id": "ID entity povinného ohrievača", + "heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne", + "heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne", + "heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne", + "heater_keep_alive": "Voliteľný interval obnovy stavu spínača ohrievača. Ak to nie je potrebné, nechajte prázdne.", + "proportional_function": "Algoritmus, ktorý sa má použiť (ďalšie algoritmy je možné nainštalovať ako {plugins_link})", + "climate_entity_id": "ID základnej klimatickej entity", + "climate_entity2_id": "2. základný identifikátor klimatickej entity", + "climate_entity3_id": "3. základný identifikátor klimatickej entity", + "climate_entity4_id": "4. základný identifikátor klimatickej entity", + "ac_mode": "Použite režim klimatizácie (AC)", + "sync_device_internal_temp": "Synchronizácia vnútornej teploty zariadenia", + "valve_entity_id": "1. ventil číslo entity id", + "valve_entity2_id": "2. ventil číslo entity id", + "valve_entity3_id": "3. ventil číslo entity id", + "valve_entity4_id": "4. ventil číslo entity id", + "auto_regulation_mode": "Automatické nastavenie cieľovej teploty", + "auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle", + "auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov", + "auto_regulation_use_device_temp": "Na urýchlenie samoregulácie použite prípadný vnútorný snímač teploty podkladu (vo väčšine prípadov nie je potrebné)", + "inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať", + "auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie" + } + }, + "tpi": { + "title": "TPI - {name}", + "description": "Časovo proporcionálne integrálne atribúty", + "data": { + "tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu", + "tpi_coef_ext": "Koeficient na použitie pre vonkajšiu teplotnú deltu", + "use_tpi_central_config": "Použite centrálnu konfiguráciu TPI" + }, + "data_description": { + "tpi_coef_int": "Koeficient na použitie pre vnútornú teplotnú deltu", + "tpi_coef_ext": "Koeficient na použitie pre deltu vonkajšej teploty", + "use_tpi_central_config": "Začiarknite, ak chcete použiť centrálnu konfiguráciu TPI. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu TPI pre tento VTherm" + } + }, + "presets": { + "title": "Predvoľby - {name}", + "description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)", + "data": { + "use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb" + } + }, + "window": { + "title": "Správa okien - {name}", + "description": "Otvoriť správu okien.\nAk sa príslušné entity_id nepoužíva, ponechajte prázdne\nMôžete tiež nakonfigurovať automatickú detekciu otvoreného okna na základe poklesu teploty", + "data": { + "window_sensor_entity_id": "ID entity snímača okna", + "window_delay": "Oneskorenie snímača okna (sekundy)", + "window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)", + "window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)", + "window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)", + "use_window_central_config": "Použite centrálnu konfiguráciu okna", + "window_action": "Akcia" + }, + "data_description": { + "window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor", + "window_delay": "Zohľadňuje sa oneskorenie v sekundách pred detekciou snímača", + "window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne", + "window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne", + "window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne", + "use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm", + "window_action": "Akcia, ktorá sa má vykonať, ak sa okno zistí ako otvorené" + } + }, + "motion": { + "title": "Riadenie pohybu - {name}", + "description": "Správa snímača pohybu. Predvoľba sa môže automaticky prepínať v závislosti od detekcie pohybu\nAk sa nepoužíva, ponechajte zodpovedajúce entity_id prázdne.\nmotion_preset a no_motion_preset by mali byť nastavené na zodpovedajúci názov predvoľby", + "data": { + "motion_sensor_entity_id": "ID entity snímača pohybu", + "motion_delay": "Oneskorenie aktivácie", + "motion_off_delay": "Oneskorenie deaktivácie", + "motion_preset": "Prednastavený pohyb", + "no_motion_preset": "Žiadna predvoľba pohybu", + "use_motion_central_config": "Použite centrálnu konfiguráciu pohybu" + }, + "data_description": { + "motion_sensor_entity_id": "ID entity snímača pohybu", + "motion_delay": "Oneskorenie aktivácie pohybu (sekundy)", + "motion_off_delay": "Oneskorenie deaktivácie pohybu (sekundy)", + "motion_preset": "Prednastavené na použitie pri detekcii pohybu", + "no_motion_preset": "Prednastavené na použitie, keď nie je detekovaný žiadny pohyb", + "use_motion_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho pohybu. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu pohybu pre tento VTherm" + } + }, + "power": { + "title": "Správa napájania - {name}", + "description": "Atribúty správy napájania.\nPoskytuje senzor výkonu a maximálneho výkonu vášho domova.\nPotom zadajte spotrebu energie ohrievača, keď je zapnutý.\nVšetky senzory a výkon zariadenia by mali mať rovnakú jednotku (kW alebo W).\nPonechajte zodpovedajúce entity_id prázdne ak sa nepoužíva.", + "data": { + "power_sensor_entity_id": "ID entity snímača výkonu", + "max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu", + "power_temp": "Teplota pre zníženie výkonu", + "use_power_central_config": "Použite centrálnu konfiguráciu napájania" + }, + "data_description": { + "power_sensor_entity_id": "ID entity snímača výkonu", + "max_power_sensor_entity_id": "ID entity snímača maximálneho výkonu", + "power_temp": "Teplota pre zníženie výkonu", + "use_power_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho napájania. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu napájania pre tento VTherm" + } + }, + "presence": { + "title": "Prítommnosť - {name}", + "description": "Atribúty riadenia prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, je niekto prítomný) a poskytuje zodpovedajúce prednastavené nastavenie teploty.", + "data": { + "presence_sensor_entity_id": "Senzor prítomnosti", + "use_presence_central_config": "Použite konfiguráciu centrálnej prítomnosti teploty. Ak chcete použiť špecifické entity teploty, zrušte začiarknutie" + }, + "data_description": { + "presence_sensor_entity_id": "ID entity senzora prítomnosti" + } + }, + "advanced": { + "title": "Pokročilé parametre - {name}", + "description": "Konfigurácia pokročilých parametrov. Ak neviete, čo robíte, ponechajte predvolené hodnoty.\nTento parameter môže viesť k veľmi zlej regulácii teploty alebo výkonu.", + "data": { + "minimal_activation_delay": "Minimálne oneskorenie aktivácie", + "safety_delay_min": "Bezpečnostné oneskorenie (v minútach)", + "safety_min_on_percent": "Minimálne percento výkonu pre bezpečnostný režim", + "safety_default_on_percent": "Percento výkonu na použitie v bezpečnostnom režime", + "repair_incorrect_state": "Opraviť nesprávny stav", + "use_advanced_central_config": "Použite centrálnu rozšírenú konfiguráciu" + }, + "data_description": { + "minimal_activation_delay": "Oneskorenie v sekundách, pri ktorom sa zariadenie neaktivuje", + "safety_delay_min": "Maximálne povolené oneskorenie v minútach medzi dvoma meraniami teploty. Po uplynutí tohto oneskorenia sa termostat prepne do bezpečnostného vypnutého stavu", + "safety_min_on_percent": "Minimálna percentuálna hodnota ohrevu pre aktiváciu prednastavenej bezpečnosti. Pod týmto percentom výkonu termostat neprejde do prednastavenia zabezpečenia", + "safety_default_on_percent": "Predvolená percentuálna hodnota vykurovacieho výkonu v bezpečnostnej predvoľbe. Nastavte na 0, ak chcete vypnúť ohrievač v zabezpečenom stave", + "repair_incorrect_state": "Automaticky opraviť nesprávny stav základných zariadení pri detekcií. Funkcia znovu pošle požadovaný príkaz, ak stav základného zariadenia nezodpovedá očakávanému stavu.", + "use_advanced_central_config": "Začiarknite, ak chcete použiť centrálnu rozšírenú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú rozšírenú konfiguráciu pre tento VTherm" + } + }, + "sync_device_internal_temp": { + "title": "Synchronize device internal temperature", + "description": "Configuration to synchronize the internal temperature of the underlying devices with selected entities", + "data": { + "sync_with_calibration": "Apply offset calibration", + "sync_entity_ids": "Entities id used for synchronization" + }, + "data_description": { + "sync_with_calibration": "Check to apply the offset calibration when synchronizing the internal temperature. Uncheck to directly copy the temperature from the selected entities", + "sync_entity_ids": "The list of the entities used to synchronize the internal temperature of the underlying devices. There should be one per underlying device. Should be a calibration entity if checkbox is selected or a temperature entity otherwise. Both are `number` entities." + } + }, + "valve_regulation": { + "title": "Samoregulácia s ventilon - {name}", + "description": "Konfigurácia samoregulácie s priamym ovládaním ventilu", + "data": { + "offset_calibration_entity_ids": "Entity kalibrácie offsetu", + "opening_degree_entity_ids": "Entity stupňa otvorenia", + "closing_degree_entity_ids": "Entity stupňa zatvorenia", + "proportional_function": "Algoritmus", + "opening_threshold_degree": "Prah otvorenia", + "min_opening_degrees": "Minimálne stupne otvorenia", + "max_opening_degrees": "Maximálny stupeň otvorenia", + "max_closing_degree": "Maximálny stupeň zatvorenia" + }, + "data_description": { + "offset_calibration_entity_ids": "Zoznam entít 'kalibrácie offsetu'. Nastavte, ak má váš TRV entitu pre lepšiu reguláciu. Mala by byť jedna na každú základnú klimatickú entitu", + "opening_degree_entity_ids": "Zoznam entít 'stupňa otvorenia'. Mala by byť jedna na každú základnú klimatickú entitu", + "closing_degree_entity_ids": "Zoznam entít 'stupňa zatvorenia'. Nastavte, ak má váš TRV entitu pre lepšiu reguláciu. Mala by byť jedna na každú základnú klimatickú entitu", + "proportional_function": "Algoritmus, ktorý sa má použiť (ďalšie algoritmy je možné nainštalovať ako {plugins_link})", + "opening_threshold_degree": "Hodnota otvorenia ventilu, pod ktorou by mal byť ventil považovaný za zatvorený (a potom bude platný 'max_closing_degree')", + "min_opening_degrees": "Minimálna hodnota stupňa otvorenia pre každé základné zariadenie, oddelená čiarkami. Predvolená hodnota je 0. Príklad: 20, 25, 30", + "max_opening_degrees": "Maximálna hodnota stupňa otvorenia pre každé základné zariadenie, oddelené čiarkami. Predvolená hodnota je 100. Ventil sa nikdy neotvorí viac ako táto hodnota. Príklad: 80, 85, 90", + "max_closing_degree": "Maximálna hodnota stupňa zatvorenia. Ventil sa nikdy nezatvorí viac ako táto hodnota. Nastavte na 100 pre úplné zatvorenie ventilu" + } + }, + "heating_failure_detection": { + "title": "Detekcia porúch vykurovania", + "description": "Konfigurácia funkcie detekcie porúch vykurovania. Detekuje anomálie, keď sa očakáva vykurovanie, ale teplota nestúpa, alebo keď je vykurovanie vypnuté, ale teplota stále stúpa.", + "data": { + "use_heating_failure_detection_feature": "Povoliť detekciu porúch vykurovania", + "use_heating_failure_detection_central_config": "Použiť centrálnu konfiguráciu detekcie porúch", + "heating_failure_threshold": "Prah poruchy vykurovania", + "cooling_failure_threshold": "Prah poruchy chladenia", + "heating_failure_detection_delay": "Oneskorenie detekcie (minúty)", + "temperature_change_tolerance": "Tolerancia zmeny teploty (°C)", + "failure_detection_enable_template": "Šablóna povolenia detekcie" + }, + "data_description": { + "use_heating_failure_detection_feature": "Povoliť detekciu anomálií vykurovania pre VTherm s TPI", + "use_heating_failure_detection_central_config": "Zaškrtnite pre použitie centrálnej konfigurácie detekcie porúch. Odškrtnite pre použitie špecifických parametrov pre tento VTherm", + "heating_failure_threshold": "Percentuálny prah zapnutia, nad ktorým by vykurovanie malo spôsobiť zvýšenie teploty (0.9 = 90%)", + "cooling_failure_threshold": "Percentuálny prah zapnutia, pod ktorým by teplota nemala stúpať (0.0 = 0%)", + "heating_failure_detection_delay": "Čas v minútach na čakanie pred kontrolou, či sa teplota zmenila podľa očakávania", + "temperature_change_tolerance": "Minimálna zmena teploty v stupňoch považovaná za významnú. Menšie zmeny sú ignorované na filtrovanie šumu snímača (predvolené: 0.5°C)", + "failure_detection_enable_template": "Voliteľná šablóna Jinja2, ktorá musí vrátiť True na povolenie detekcie. Užitočné na dočasné vypnutie detekcie, keď je aktívny externý zdroj tepla (napr. `is_state('binary_sensor.pec_na_drevo', 'off')` v dvojitých zložených zátvorkách). Ak je prázdne, detekcia je vždy povolená." + } + } + }, + "error": { + "unknown": "Neočakávaná chyba", + "unknown_entity": "Neznáme ID entity", + "window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje", + "no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“.", + "service_configuration_format": "Formát konfigurácie služby je nesprávny", + "valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings", + "sync_device_internal_temp_nb_entities_incorrect": "The number of entities for synchronize device internal temperature should be equal to the number of underlyings", + "min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30", + "max_opening_degrees_format": "Očakáva sa zoznam kladných celých čísel medzi 0 a 100 oddelených čiarkou. Príklad: 80, 85, 90", + "min_max_opening_degrees_inconsistent": "Pre každé základné zariadenie musí byť max_opening_degrees striktne väčší ako min_opening_degrees. Skontrolujte prosím svoju konfiguráciu.", + "vswitch_configuration_incorrect": "The command customization configuration is incorrect. It is required for underlying entities that are not switches, and the format must be service_name[/attribute:value]. More information is available in the README" + }, + "abort": { + "already_configured": "Zariadenie je už nakonfigurované" + } + }, + "selector": { + "thermostat_type": { + "options": { + "thermostat_central_config": "Centrálna konfigurácia", + "thermostat_over_switch": "Termostat nad spínačom", + "thermostat_over_climate": "Termostat nad iným termostatom", + "thermostat_over_valve": "Termostat nad ventilom" + } + }, + "auto_regulation_mode": { + "options": { + "auto_regulation_slow": "Pomalé", + "auto_regulation_strong": "Silné", + "auto_regulation_medium": "Stredné", + "auto_regulation_light": "Jemné", + "auto_regulation_expert": "Expertné", + "auto_regulation_none": "Nie auto-regulácia" + } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "Žiadny automatický ventilátor", + "auto_fan_low": "Nízky", + "auto_fan_medium": "Stredný", + "auto_fan_high": "Vysoký", + "auto_fan_turbo": "Turbo" + } + }, + "window_action": { + "options": { + "window_turn_off": "Vypnúť", + "window_fan_only": "Len ventilátor", + "window_frost_temp": "Ochrana pred mrazom", + "window_eco_temp": "Eco" + } + }, + "presets": { + "options": { + "frost": "Ochrana proti mrazu", + "eco": "Eco", + "comfort": "Komfort", + "boost": "Boost" + } + } + }, + "entity": { + "climate": { + "versatile_thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "power": "Vyradenie", + "security": "Zabezpečenie", + "none": "Manuálne" + } + } + } + } + }, + "number": { + "frost_temp": { + "name": "Mráz" + }, + "eco_temp": { + "name": "Eco" + }, + "comfort_temp": { + "name": "Komfort" + }, + "boost_temp": { + "name": "Boost" + }, + "frost_ac_temp": { + "name": "Mráz ac" + }, + "eco_ac_temp": { + "name": "Eco ac" + }, + "comfort_ac_temp": { + "name": "Komfort ac" + }, + "boost_ac_temp": { + "name": "Boost ac" + }, + "frost_away_temp": { + "name": "Mráz mimo" + }, + "eco_away_temp": { + "name": "Eko mimo" + }, + "comfort_away_temp": { + "name": "Komfort mimo" + }, + "boost_away_temp": { + "name": "Boost mimo" + }, + "eco_ac_away_temp": { + "name": "Eco ac mimo" + }, + "comfort_ac_away_temp": { + "name": "Komfort ac mimo" + }, + "boost_ac_away_temp": { + "name": "Boost ac mimo" + } + } + } +} \ No newline at end of file diff --git a/custom_components/versatile_thermostat/translations/zh-Hans.json b/custom_components/versatile_thermostat/translations/zh-Hans.json new file mode 100644 index 0000000..5dd4bfd --- /dev/null +++ b/custom_components/versatile_thermostat/translations/zh-Hans.json @@ -0,0 +1,1049 @@ +{ + "title": "多功能恒温器配置", + "config": { + "flow_title": "多功能恒温器配置", + "step": { + "user": { + "title": "多功能恒温器类型", + "description": "选择要创建的恒温器类型", + "data": { + "thermostat_type": "恒温器类型" + }, + "data_description": { + "thermostat_type": "只能使用一种中央配置类型" + } + }, + "menu": { + "title": "菜单", + "description": "配置你的恒温器。输入所有必填参数后即可完成配置。", + "menu_options": { + "main": "主要属性", + "central_boiler": "中央锅炉", + "type": "底层设备", + "tpi": "TPI 参数", + "features": "功能", + "presets": "预设", + "window": "窗户检测", + "motion": "运动检测", + "power": "功率管理", + "presence": "在家状态检测", + "advanced": "高级参数", + "auto_start_stop": "自动启动和停止", + "valve_regulation": "阀门调节配置", + "sync_device_internal_temp": "同步设备温度", + "lock": "锁定", + "heating_failure_detection": "加热故障检测", + "auto_tpi_configuration": "Auto TPI - 配置", + "auto_tpi_avg_settings": "Auto TPI - 加权平均", + "auto_tpi_ema_settings": "Auto TPI - EWMA", + "finalize": "全部完成", + "configuration_not_complete": "配置未完成" + } + }, + "lock": { + "title": "锁管理", + "description": "锁定功能可防止 UI 或自动化更改恒温器的配置,同时保持恒温器运行。", + "data": { + "use_lock_central_config": "使用中控锁配置", + "lock_code": "可选 PIN 码(4 位数字)", + "lock_users": "锁定将阻止用户发出命令", + "lock_automations": "锁定将阻止来自自动化和集成(即调度程序)的命令", + "auto_relock_sec": "自动重锁延迟" + }, + "data_description": { + "use_lock_central_config": "勾选使用中控锁配置。取消选中为此 VTherm 使用特定的锁定配置", + "auto_relock_sec": "解锁后自动重新锁定之前的延迟秒数。设置为 0 以禁用自动重新锁定。默认值为 30 秒。" + } + }, + "main": { + "title": "添加新的多功能恒温器", + "description": "主要必填属性", + "data": { + "name": "名称", + "thermostat_type": "恒温器类型", + "temperature_sensor_entity_id": "室内温度", + "last_seen_temperature_sensor_entity_id": "最近一次室内温度更新时间", + "external_temperature_sensor_entity_id": "室外温度传感器实体 ID", + "cycle_min": "循环持续时间(分钟)", + "temp_min": "允许的最低温度", + "temp_max": "允许的最高温度", + "step_temperature": "温度步进", + "device_power": "设备功率", + "use_central_mode": "启用中央实体控制(需要中央配置)。勾选后可使用 central_mode 选择实体控制此 VTherm。", + "use_main_central_config": "使用额外的中央主配置。勾选后使用中央主配置(室外温度、最小值、最大值、步进等)。", + "used_by_controls_central_boiler": "用于中央锅炉。勾选后,此 VTherm 可以控制中央锅炉" + }, + "data_description": { + "cycle_min": "一个加热/冷却循环的最短持续时间(以分钟为单位)以避免过于频繁的切换", + "temp_min": "恒温器允许的最低温度", + "temp_max": "恒温器允许的最高温度", + "step_temperature": "设置目标温度的温度步长", + "temperature_sensor_entity_id": "室温传感器实体 ID", + "last_seen_temperature_sensor_entity_id": "室温传感器最后更新时间实体 ID。应为 datetime 传感器", + "external_temperature_sensor_entity_id": "室外温度传感器实体 ID。如果选择中央配置则不使用" + } + }, + "features": { + "title": "功能", + "description": "恒温器功能", + "data": { + "use_window_feature": "使用窗户检测", + "use_motion_feature": "使用运动检测", + "use_power_feature": "使用功率管理", + "use_presence_feature": "使用在家状态检测", + "use_central_boiler_feature": "使用中央锅炉。勾选后为中央锅炉添加控制。此复选框生效后,还需要配置将控制中央锅炉的 VTherm。如果某个 VTherm 需要加热,锅炉将开启;如果没有 VTherm 需要加热,锅炉将关闭。开启/关闭中央锅炉的命令在相关配置页面中设置。", + "use_auto_start_stop_feature": "使用自动启停功能", + "use_heating_failure_detection_feature": "使用加热故障检测" + } + }, + "type": { + "title": "链接实体", + "description": "链接实体属性", + "data": { + "underlying_entity_ids": "需要控制的设备", + "heater_keep_alive": "开关保活间隔(秒)", + "proportional_function": "算法", + "ac_mode": "空调模式", + "sync_device_internal_temp": "同步设备内部温度", + "auto_regulation_mode": "自调节", + "auto_regulation_dtemp": "调节阈值", + "auto_regulation_periode_min": "调节最短周期", + "auto_regulation_use_device_temp": "使用底层内部温度", + "inverse_switch_command": "反向开关指令", + "auto_fan_mode": "自动风扇模式", + "on_command_text": "开启命令自定义", + "vswitch_on_command": "可选开启命令", + "off_command_text": "关闭命令自定义", + "vswitch_off_command": "可选关闭命令" + }, + "data_description": { + "underlying_entity_ids": "要控制的设备 - 1 为必填项", + "heater_keep_alive": "可选的加热器开关状态刷新间隔。不需要时请留空。", + "proportional_function": "要使用的算法(其他算法可以作为{plugins_link}安装)", + "ac_mode": "使用空调(AC)模式", + "sync_device_internal_temp": "勾选同步底层设备内部温度(填写新菜单项“同步设备温度”进行配置)", + "auto_regulation_mode": "自动调节目标温度", + "auto_regulation_dtemp": "以 °(或阀门的 %)为单位的阈值,低于该阈值将不会发送温度变化", + "auto_regulation_periode_min": "两次调节更新之间的时间间隔(分钟)", + "auto_regulation_use_device_temp": "使用底层设备可能提供的内部温度传感器来加速自调节(大多数情况下不需要)", + "inverse_switch_command": "对于带导引线和二极管的开关,可能需要反转命令", + "auto_fan_mode": "当需要大量加热/冷却时自动启动风扇", + "on_command_text": "对于 `select` 或 `climate` 类型的底层,您必须自定义命令。" + } + }, + "tpi": { + "title": "TPI", + "description": "时间比例积分属性", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "tpi_threshold_low": "TPI 阈值低", + "tpi_threshold_high": "TPI 高阈值", + "use_tpi_central_config": "使用中央TPI配置", + "minimal_activation_delay": "最小激活延迟", + "minimal_deactivation_delay": "最小停用延迟", + "auto_tpi_mode": "启用自动 TPI 学习" + }, + "data_description": { + "tpi_coef_int": "用于内部温度增量的系数", + "tpi_coef_ext": "用于外部温度增量的系数", + "tpi_threshold_low": "以°为单位的阈值,在该阈值下,TPI 算法将开启。 0表示无阈值", + "tpi_threshold_high": "以°为单位的阈值,高于该阈值,TPI 算法将关闭。 0表示无阈值", + "use_tpi_central_config": "选中以使用中央 TPI 配置。取消选中为此 VTherm 使用特定的 TPI 配置", + "minimal_activation_delay": "设备不会被激活的延迟秒数", + "minimal_deactivation_delay": "设备保持活动状态的延迟(以秒为单位)", + "auto_tpi_mode": "TPI 学习会话需要手动启动,请先阅读文档。" + } + }, + "presets": { + "title": "预设", + "description": "选择恒温器是否使用中央预设 - 取消选择恒温器将拥有自己的预设", + "data": { + "use_presets_central_config": "使用中央预设配置" + } + }, + "window": { + "title": "窗户管理", + "description": "窗户管理。\n也可以根据温度下降配置自动开窗检测", + "data": { + "window_sensor_entity_id": "窗户传感器实体 ID", + "window_delay": "窗户传感器“开启”延迟(秒)", + "window_off_delay": "窗户传感器“关闭”延迟(秒)", + "window_auto_open_threshold": "自动开窗检测的温度下降阈值(单位为°/小时)", + "window_auto_close_threshold": "自动检测结束的温升阈值(单位为°/小时)", + "window_auto_max_duration": "自动开窗检测最大持续时间(分钟)", + "use_window_central_config": "使用中央窗户配置", + "window_action": "操作" + }, + "data_description": { + "window_sensor_entity_id": "如果不使用窗户传感器并使用自动检测,则留空", + "window_delay": "考虑传感器“开启”检测之前的延迟(以秒为单位)", + "window_off_delay": "传感器“关闭”检测被采纳前的延迟(秒)。留空则使用与开启延迟相同的值", + "window_auto_open_threshold": "建议值:3 到 10 之间。如果不使用自动开窗检测,则留空", + "window_auto_close_threshold": "建议值:0。如果不使用自动开窗检测,则留空", + "window_auto_max_duration": "建议值:60(一小时)。如果不使用自动窗户打开检测,则留空", + "use_window_central_config": "选择使用中央窗户配置。取消选择则为此 VTherm 使用特定窗户配置", + "window_action": "检测到窗户打开时执行的操作" + } + }, + "motion": { + "title": "运动管理", + "description": "运动传感器管理。预设可根据运动检测自动切换\nmotion_preset 和 no_motion_preset 应设置为对应的预设名称", + "data": { + "motion_sensor_entity_id": "运动传感器实体 ID", + "motion_delay": "激活延迟", + "motion_off_delay": "停用延迟", + "motion_preset": "运动预设", + "no_motion_preset": "无运动预设", + "use_motion_central_config": "使用中央运动配置" + }, + "data_description": { + "motion_sensor_entity_id": "运动传感器的实体 ID", + "motion_delay": "运动激活延迟(秒)", + "motion_off_delay": "运动停用延迟(秒)", + "motion_preset": "检测到运动时使用的预设", + "no_motion_preset": "未检测到运动时使用的预设", + "use_motion_central_config": "选中以使用中央运动配置。取消选中为此 VTherm 使用特定运动配置" + } + }, + "power": { + "title": "功率管理", + "description": "电源管理属性。\n提供您家的功率和最大功率传感器。\n指定加热器开启时的功耗。\n所有传感器和设备功率应使用相同的单位(kW 或 W)", + "data": { + "power_sensor_entity_id": "功率", + "max_power_sensor_entity_id": "最大功率", + "power_temp": "削峰温度", + "use_power_central_config": "采用集中电源配置" + }, + "data_description": { + "power_sensor_entity_id": "功率传感器实体 ID", + "max_power_sensor_entity_id": "最大功率传感器实体 ID", + "power_temp": "削峰温度", + "use_power_central_config": "勾选使用中央电源配置。取消选中为此 VTherm 使用特定电源配置" + } + }, + "presence": { + "title": "在家状态管理", + "description": "在家状态管理属性。\n提供家中的在家状态传感器(true 表示有人在家),并设置对应的温度预设", + "data": { + "presence_sensor_entity_id": "在家状态传感器", + "use_presence_central_config": "使用中央在家状态温度配置。取消选择则使用特定温度实体" + }, + "data_description": { + "presence_sensor_entity_id": "在家状态传感器实体 ID" + } + }, + "advanced": { + "title": "高级参数", + "description": "高级参数配置。如果您不知道自己在做什么,请保留默认值。\n这些参数可能导致温度控制很差或功率调节很差", + "data": { + "safety_delay_min": "安全延时(分钟)", + "safety_min_on_percent": "启用安全模式的最低功率百分比", + "safety_default_on_percent": "安全模式下使用的功率百分比", + "repair_incorrect_state": "修复不正确的状态", + "use_advanced_central_config": "使用中央高级配置" + }, + "data_description": { + "safety_delay_min": "两次温度测量之间允许的最大延迟(以分钟为单位)。超过此延迟,恒温器将转至安全关闭状态", + "safety_min_on_percent": "安全预设激活的最小加热百分比值。低于此功率百分比时,恒温器将不会进入安全预设状态", + "safety_default_on_percent": "安全预设中的默认加热功率百分比值。设置为 0 以在安全预设中关闭加热器", + "repair_incorrect_state": "检测到时自动修复底层实体的错误状态。如果底层设备状态与预期状态不匹配,该功能将重新发送所需的命令。", + "use_advanced_central_config": "勾选使用中央高级配置。取消选中为此 VTherm 使用特定的高级配置" + } + }, + "central_boiler": { + "title": "中央锅炉控制", + "description": "输入要调用的服务以打开/关闭中央锅炉。调用的服务必须格式如下:`entity_id/service_name[/attribute:value]`(/attribute:value 是可选的)\n例如:\n- 打开开关:`switch.controle_chaudiere/switch.turn_on`\n- 关闭开关:`switch.controle_chaudiere/switch.turn_off`\n- 将锅炉编程至 25°,从而强制其点火:`climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- 向锅炉发送 10°,从而强制其熄灭:`climate.thermostat_chaudiere/climate.set_temperature/temperature:10`", + "data": { + "central_boiler_activation_service": "开启命令", + "central_boiler_deactivation_service": "关闭命令", + "central_boiler_activation_delay_sec": "激活延迟(秒)", + "keep_alive_boiler_delay_sec": "保活延迟(秒)" + }, + "data_description": { + "central_boiler_activation_service": "开启中央锅炉的命令,格式类似 entity_id/service_name[/attribute:value]", + "central_boiler_deactivation_service": "关闭中央锅炉的命令,格式类似 entity_id/service_name[/attribute:value]", + "central_boiler_activation_delay_sec": "当需要打开中央锅炉时,在激活中央锅炉之前延迟几秒", + "keep_alive_boiler_delay_sec": "如果启用了保持活动状态,则定期重新发送锅炉命令的延迟(以秒为单位)。设置为 0 以禁用保持活动状态。" + } + }, + "valve_regulation": { + "title": "带阀自调节", + "description": "通过直接控制阀门进行自我调节的配置", + "data": { + "offset_calibration_entity_ids": "偏移校准实体", + "opening_degree_entity_ids": "开度实体", + "closing_degree_entity_ids": "闭合度实体", + "proportional_function": "算法", + "opening_threshold_degree": "开启阈值度数", + "min_opening_degrees": "最小开度", + "max_opening_degrees": "最大开度", + "max_closing_degree": "最大关闭度" + }, + "data_description": { + "offset_calibration_entity_ids": "“偏移校准”实体列表。如果你的 TRV 提供该实体,请设置它以获得更好的调节效果。每个底层 climate 实体应对应一个。", + "opening_degree_entity_ids": "“开度”实体列表。每个底层 climate 实体应对应一个。", + "closing_degree_entity_ids": "“闭合度”实体列表。如果你的 TRV 提供该实体,请设置它以获得更好的调节效果。每个底层 climate 实体应对应一个。", + "proportional_function": "要使用的算法(其他算法可以作为{plugins_link}安装)", + "opening_threshold_degree": "阀门开度低于此值时应视为关闭(并应用 max_closing_degree)", + "min_opening_degrees": "当超过先前阈值时每个底层设备的开度最小值,以逗号分隔。默认为 0。示例:20,25,30", + "max_opening_degrees": "开度最大值。阀门的开度永远不会超过该值", + "max_closing_degree": "关闭度最大值。阀门永远不会在高于该值时关闭。将其设置为 100 以完全关闭阀门" + } + }, + "auto_tpi_configuration": { + "title": "Auto TPI - 配置", + "description": "自动 TPI 学习的常规设置。", + "data": { + "auto_tpi_learning_type": "学习类型", + "heater_heating_time": "加热时间(分钟)", + "heater_cooling_time": "冷却时间(分钟)", + "auto_tpi_heating_rate": "升温速率({unit}/h)", + "auto_tpi_aggressiveness": "激进程度", + "auto_tpi_enable_advanced_settings": "启用高级参数", + "auto_tpi_continuous_kext": "持续学习 Kext" + }, + "data_description": { + "auto_tpi_learning_type": "选择“发现”用于初始启动(加权平均,权重 1),或选择“微调”用于持续调整(EWMA,Alpha 0.08)。", + "heater_heating_time": "达到满功率的时间(分钟)", + "heater_cooling_time": "停止后的冷却时间(分钟)\n\n| 类型 | 加热时间 | 冷却时间 |\n| :--- | :--- | :--- |\n| 电暖器 | 5mn | 7mn |\n| 水暖散热器 | 15mn | 20mn |\n| 地暖 | 30mn | 45mn |", + "auto_tpi_heating_rate": "散热器升温能力({unit}每小时)。保留为 0 以进行自动学习(引导程序)。", + "auto_tpi_aggressiveness": "学习系数的缩减因子 (50-100%)。较低的值会产生更保守的系数并降低超调风险。", + "auto_tpi_enable_advanced_settings": "勾选后可访问参数微调(Alpha、衰减、权重)。", + "auto_tpi_continuous_kext": "选中以启用 AutoTPI 会话之外的外部系数 (Kext) 的持续学习。" + } + }, + "auto_tpi_avg_settings": { + "title": "Auto TPI - 加权平均", + "description": "加权平均法参数。", + "data": { + "auto_tpi_avg_initial_weight": "初始权重" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "平均值计算中初始/前一个值的权重" + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA", + "description": "EWMA 方法的参数。\n\n建议:\n| 场景 | Alpha (ema_alpha) | 衰减率 (ema_decay_rate) |\n| :--- | :--- | :--- |\n| 初始学习 | 0.15 | 0.08 |\n| 精细学习 | 0.08 | 0.12 |\n| 持续学习 | 0.05 | 0.02 |", + "data": { + "auto_tpi_ema_alpha": "Alpha", + "auto_tpi_ema_decay_rate": "衰减率", + "auto_tpi_continuous_kext_alpha": "连续 Kext Alpha" + }, + "data_description": { + "auto_tpi_ema_alpha": "平滑因子 (0-1)。更高=更快的适应", + "auto_tpi_ema_decay_rate": "Alpha 随时间下降的速率(稳定)", + "auto_tpi_continuous_kext_alpha": "用于连续 Kext 学习的平滑因子 (Alpha)。默认 0.04(大约 3-5 天适应)。" + } + }, + "sync_device_internal_temp": { + "title": "同步设备内部温度", + "description": "配置以将底层设备的内部温度与所选实体同步", + "data": { + "sync_with_calibration": "应用偏移校准", + "sync_entity_ids": "用于同步的实体 ID" + }, + "data_description": { + "sync_with_calibration": "勾选以在同步内部温度时应用偏移校准。取消选中可直接复制所选实体的温度", + "sync_entity_ids": "用于同步底层设备内部温度的实体列表。每个底层设备应该有一个。如果选中复选框,则应为校准实体,否则应为温度实体。两者都是 `number` 实体。" + } + }, + "heating_failure_detection": { + "title": "加热故障检测", + "description": "配置加热故障检测功能。当预期加热但温度不升高时,或者当加热关闭但温度持续升高时,该功能会检测到异常情况。", + "data": { + "use_heating_failure_detection_feature": "启用加热故障检测", + "use_heating_failure_detection_central_config": "使用中央加热故障检测配置", + "heating_failure_threshold": "加热故障阈值", + "cooling_failure_threshold": "冷却故障阈值", + "heating_failure_detection_delay": "检测延迟(分钟)", + "temperature_change_tolerance": "温度变化容限 (°C)", + "failure_detection_enable_template": "检测启用模板" + }, + "data_description": { + "use_heating_failure_detection_feature": "使用 TPI 启用 VTherms 加热异常检测", + "use_heating_failure_detection_central_config": "勾选使用中央加热故障检测配置。取消选中为此 VTherm 使用特定参数", + "heating_failure_threshold": "开启百分比阈值,高于该阈值加热将导致温度升高 (0.9 = 90%)", + "cooling_failure_threshold": "开启百分比阈值,低于该阈值温度不应升高 (0.0 = 0%)", + "heating_failure_detection_delay": "检查温度是否按预期变化前需要等待的时间(分钟)", + "temperature_change_tolerance": "考虑显著的最小温度变化度。忽略较小的变化以过滤传感器噪声(默认值:0.5°C)", + "failure_detection_enable_template": "可选的 Jinja2 模板,必须返回 True 才能启用检测。当外部热源处于活动状态时(例如,双花括号中的 `is_state('binary_sensor.wood_stove', 'off')`),可用于暂时禁用检测。如果为空,则始终启用检测。" + } + } + }, + "error": { + "unknown": "意外错误", + "unknown_entity": "未知实体 ID", + "window_open_detection_method": "只能使用一种开窗检测方法:使用窗户传感器,或通过温度阈值自动检测,不能同时使用。", + "no_central_config": "您无法选中“使用中央配置”,因为未找到中央配置。您需要创建一个“中央配置”类型的多功能恒温器才能使用它。", + "service_configuration_format": "服务配置格式错误", + "valve_regulation_nb_entities_incorrect": "阀门调节的阀门实体数量应等于底层设备数量", + "sync_device_internal_temp_nb_entities_incorrect": "同步设备内部温度的实体数量应等于底层数量", + "min_opening_degrees_format": "需要逗号分隔的正整数列表。示例:20,25,30", + "max_opening_degrees_format": "需要一个介于 0 和最大阀门开度之间的逗号分隔的正整数列表。示例:80,85,90", + "min_max_opening_degrees_inconsistent": "对于每个底层设备,max_opening_degrees 必须严格大于 min_opening_degrees。请检查你的配置。", + "vswitch_configuration_incorrect": "命令自定义配置不正确。对于不是 switch 的底层实体,这是必需的,格式必须为 service_name[/attribute:value]。更多信息请参阅 README。" + }, + "abort": { + "already_configured": "设备已配置" + } + }, + "options": { + "flow_title": "多功能恒温器配置", + "step": { + "user": { + "title": "类型 - {name}", + "description": "选择要创建的恒温器类型", + "data": { + "thermostat_type": "恒温器类型" + }, + "data_description": { + "thermostat_type": "只能使用一种中央配置类型" + } + }, + "menu": { + "title": "菜单", + "description": "配置你的恒温器。输入所有必填参数后即可完成配置。", + "menu_options": { + "main": "主要属性", + "central_boiler": "中央锅炉", + "type": "底层设备", + "tpi": "TPI 参数", + "features": "功能", + "presets": "预设", + "window": "窗户检测", + "motion": "运动检测", + "power": "功率管理", + "presence": "在家状态检测", + "advanced": "高级参数", + "auto_start_stop": "自动启动和停止", + "valve_regulation": "阀门调节配置", + "sync_device_internal_temp": "同步设备温度", + "lock": "锁定", + "heating_failure_detection": "加热故障检测", + "finalize": "全部完成", + "configuration_not_complete": "配置未完成" + } + }, + "lock": { + "title": "锁管理 - {name}", + "description": "锁定功能可防止 UI 或自动化更改恒温器的配置,同时保持恒温器运行。", + "data": { + "use_lock_central_config": "使用中控锁配置", + "lock_code": "可选 PIN 码(4 位数字)", + "lock_users": "锁定将阻止用户发出命令", + "lock_automations": "锁定将阻止来自自动化和集成(即调度程序)的命令", + "auto_relock_sec": "自动重锁延迟" + }, + "data_description": { + "use_lock_central_config": "勾选使用中控锁配置。取消选中为此 VTherm 使用特定的锁定配置", + "auto_relock_sec": "解锁后自动重新锁定之前的延迟秒数。设置为 0 以禁用自动重新锁定。默认值为 30 秒。" + } + }, + "main": { + "title": "主要 - {name}", + "description": "主要必填属性", + "data": { + "name": "名称", + "thermostat_type": "恒温器类型", + "temperature_sensor_entity_id": "室内温度", + "last_seen_temperature_sensor_entity_id": "最近一次室内温度更新时间", + "external_temperature_sensor_entity_id": "室外温度传感器实体 ID", + "cycle_min": "循环持续时间(分钟)", + "temp_min": "允许的最低温度", + "temp_max": "允许的最高温度", + "step_temperature": "温度步进", + "device_power": "设备功率", + "use_central_mode": "启用中央实体控制(需要中央配置)。勾选后可使用 central_mode 选择实体控制此 VTherm。", + "use_main_central_config": "使用额外的中央主配置。勾选后使用中央主配置(室外温度、最小值、最大值、步进等)。", + "used_by_controls_central_boiler": "用于中央锅炉。勾选后,此 VTherm 可以控制中央锅炉" + }, + "data_description": { + "cycle_min": "一个加热/冷却循环的最短持续时间(以分钟为单位)以避免过于频繁的切换", + "temp_min": "恒温器允许的最低温度", + "temp_max": "恒温器允许的最高温度", + "step_temperature": "设置目标温度的温度步长", + "temperature_sensor_entity_id": "室温传感器实体 ID", + "last_seen_temperature_sensor_entity_id": "室温传感器最后更新时间实体 ID。应为 datetime 传感器", + "external_temperature_sensor_entity_id": "室外温度传感器实体 ID。如果选择中央配置则不使用" + } + }, + "features": { + "title": "特点 - {name}", + "description": "恒温器功能", + "data": { + "use_window_feature": "使用窗户检测", + "use_motion_feature": "使用运动检测", + "use_power_feature": "使用功率管理", + "use_presence_feature": "使用在家状态检测", + "use_central_boiler_feature": "使用中央锅炉。勾选后为中央锅炉添加控制。此复选框生效后,还需要配置将控制中央锅炉的 VTherm。如果某个 VTherm 需要加热,锅炉将开启;如果没有 VTherm 需要加热,锅炉将关闭。开启/关闭中央锅炉的命令在相关配置页面中设置。", + "use_auto_start_stop_feature": "使用自动启停功能", + "use_heating_failure_detection_feature": "使用加热故障检测" + } + }, + "type": { + "title": "实体 - {name}", + "description": "链接实体属性", + "data": { + "underlying_entity_ids": "需要控制的设备", + "heater_keep_alive": "开关保活间隔(秒)", + "proportional_function": "算法", + "ac_mode": "空调模式", + "sync_device_internal_temp": "同步设备温度", + "auto_regulation_mode": "自调节", + "auto_regulation_dtemp": "调节阈值", + "auto_regulation_periode_min": "调节最短周期", + "auto_regulation_use_device_temp": "使用底层内部温度", + "inverse_switch_command": "反向开关指令", + "auto_fan_mode": "自动风扇模式", + "on_command_text": "开启命令自定义", + "vswitch_on_command": "可选开启命令", + "off_command_text": "关闭命令自定义", + "vswitch_off_command": "可选关闭命令" + }, + "data_description": { + "underlying_entity_ids": "要控制的设备 - 1 为必填项", + "heater_keep_alive": "可选的加热器开关状态刷新间隔。不需要时请留空。", + "proportional_function": "要使用的算法(其他算法可以作为{plugins_link}安装)", + "ac_mode": "使用空调(AC)模式", + "sync_device_internal_temp": "勾选同步底层设备内部温度(填写新菜单项“同步设备温度”进行配置)", + "auto_regulation_mode": "自动调节目标温度", + "auto_regulation_dtemp": "以 °(或阀门的 %)为单位的阈值,低于该阈值将不会发送温度变化", + "auto_regulation_periode_min": "两次调节更新之间的时间间隔(分钟)", + "auto_regulation_use_device_temp": "使用底层设备可能提供的内部温度传感器来加速自调节(大多数情况下不需要)", + "inverse_switch_command": "对于带导引线和二极管的开关,可能需要反转命令", + "auto_fan_mode": "当需要大量加热/冷却时自动启动风扇", + "on_command_text": "对于 `select` 或 `climate` 类型的底层,您必须自定义命令。" + } + }, + "tpi": { + "title": "TPI - {name}", + "description": "时间比例积分属性", + "data": { + "tpi_coef_int": "coef_int", + "tpi_coef_ext": "coef_ext", + "tpi_threshold_low": "TPI 阈值低", + "tpi_threshold_high": "TPI 高阈值", + "use_tpi_central_config": "使用中央TPI配置", + "minimal_activation_delay": "最小激活延迟", + "minimal_deactivation_delay": "最小停用延迟", + "auto_tpi_mode": "启用自动 TPI 学习" + }, + "data_description": { + "tpi_coef_int": "用于内部温度增量的系数", + "tpi_coef_ext": "用于外部温度增量的系数", + "tpi_threshold_low": "以°为单位的阈值,在该阈值下,TPI 算法将开启。 0表示无阈值", + "tpi_threshold_high": "以°为单位的阈值,高于该阈值,TPI 算法将关闭。 0表示无阈值", + "use_tpi_central_config": "选中以使用中央 TPI 配置。取消选中为此 VTherm 使用特定的 TPI 配置", + "minimal_activation_delay": "设备不会被激活的延迟秒数", + "minimal_deactivation_delay": "设备保持活动状态的延迟(以秒为单位)", + "auto_tpi_mode": "TPI 学习会话需要手动启动,请先阅读文档。" + } + }, + "presets": { + "title": "预设 - {name}", + "description": "勾选后恒温器将使用中央预设。取消勾选则恒温器会拥有自己的预设实体", + "data": { + "use_presets_central_config": "使用中央预设配置" + } + }, + "window": { + "title": "窗户 - {name}", + "description": "窗户管理。\n也可以根据温度下降配置自动开窗检测", + "data": { + "window_sensor_entity_id": "窗户传感器实体 ID", + "window_delay": "窗户传感器“开启”延迟(秒)", + "window_off_delay": "窗户传感器“关闭”延迟(秒)", + "window_auto_open_threshold": "自动开窗检测的温度下降阈值(单位为°/小时)", + "window_auto_close_threshold": "自动检测结束的温升阈值(单位为°/小时)", + "window_auto_max_duration": "自动开窗检测最大持续时间(分钟)", + "use_window_central_config": "使用中央窗户配置", + "window_action": "操作" + }, + "data_description": { + "window_sensor_entity_id": "如果不使用窗户传感器并使用自动检测,则留空", + "window_delay": "考虑传感器“开启”检测之前的延迟(以秒为单位)", + "window_off_delay": "传感器“关闭”检测被采纳前的延迟(秒)。留空则使用与开启延迟相同的值", + "window_auto_open_threshold": "建议值:3 到 10 之间。如果不使用自动开窗检测,则留空", + "window_auto_close_threshold": "建议值:0。如果不使用自动开窗检测,则留空", + "window_auto_max_duration": "建议值:60(一小时)。如果不使用自动窗户打开检测,则留空", + "use_window_central_config": "选择使用中央窗户配置。取消选择则为此 VTherm 使用特定窗户配置", + "window_action": "检测到窗户打开时执行的操作" + } + }, + "motion": { + "title": "运动 - {name}", + "description": "运动传感器管理。预设可根据运动检测自动切换\nmotion_preset 和 no_motion_preset 应设置为对应的预设名称", + "data": { + "motion_sensor_entity_id": "运动传感器实体 ID", + "motion_delay": "激活延迟", + "motion_off_delay": "停用延迟", + "motion_preset": "运动预设", + "no_motion_preset": "无运动预设", + "use_motion_central_config": "使用中央运动配置" + }, + "data_description": { + "motion_sensor_entity_id": "运动传感器的实体 ID", + "motion_delay": "运动激活延迟(秒)", + "motion_off_delay": "运动停用延迟(秒)", + "motion_preset": "检测到运动时使用的预设", + "no_motion_preset": "未检测到运动时使用的预设", + "use_motion_central_config": "选中以使用中央运动配置。取消选中为此 VTherm 使用特定运动配置" + } + }, + "power": { + "title": "功率 - {name}", + "description": "电源管理属性。\n提供您家的功率和最大功率传感器。\n指定加热器开启时的功耗。\n所有传感器和设备功率应使用相同的单位(kW 或 W)", + "data": { + "power_sensor_entity_id": "功率", + "max_power_sensor_entity_id": "最大功率", + "power_temp": "削峰温度", + "use_power_central_config": "采用集中电源配置" + }, + "data_description": { + "power_sensor_entity_id": "功率传感器实体 ID", + "max_power_sensor_entity_id": "最大功率传感器实体 ID", + "power_temp": "削峰温度", + "use_power_central_config": "勾选使用中央电源配置。取消选中为此 VTherm 使用特定电源配置" + } + }, + "presence": { + "title": "在家状态 - {name}", + "description": "在家状态管理属性。\n提供家中的在家状态传感器(true 表示有人在家),并设置对应的温度预设", + "data": { + "presence_sensor_entity_id": "在家状态传感器", + "use_presence_central_config": "使用中央在家状态温度配置。取消选择则使用特定温度实体" + }, + "data_description": { + "presence_sensor_entity_id": "在家状态传感器实体 ID" + } + }, + "advanced": { + "title": "高级 - {name}", + "description": "高级参数配置。如果您不知道自己在做什么,请保留默认值。\n这些参数可能导致温度控制很差或功率调节很差", + "data": { + "minimal_activation_delay": "最小激活延迟", + "minimal_deactivation_delay": "最小停用延迟", + "safety_delay_min": "安全延时(分钟)", + "safety_min_on_percent": "启用安全模式的最低功率百分比", + "safety_default_on_percent": "安全模式下使用的功率百分比", + "repair_incorrect_state": "修复不正确的状态", + "use_advanced_central_config": "使用中央高级配置" + }, + "data_description": { + "minimal_activation_delay": "设备不会被激活的延迟秒数", + "minimal_deactivation_delay": "设备保持活动状态的延迟(以秒为单位)", + "safety_delay_min": "两次温度测量之间允许的最大延迟(以分钟为单位)。超过此延迟,恒温器将转至安全关闭状态", + "safety_min_on_percent": "安全预设激活的最小加热百分比值。低于此功率百分比时,恒温器将不会进入安全预设状态", + "safety_default_on_percent": "安全预设中的默认加热功率百分比值。设置为 0 以在安全预设中关闭加热器", + "repair_incorrect_state": "检测到时自动修复底层实体的错误状态。如果底层设备状态与预期状态不匹配,该功能将重新发送所需的命令。", + "use_advanced_central_config": "勾选使用中央高级配置。取消选中为此 VTherm 使用特定的高级配置" + } + }, + "central_boiler": { + "title": "中央锅炉控制 - {name}", + "description": "输入要调用的服务以打开/关闭中央锅炉。调用的服务必须格式如下:`entity_id/service_name[/attribute:value]`(/attribute:value 是可选的)\n例如:\n- 打开开关:`switch.controle_chaudiere/switch.turn_on`\n- 关闭开关:`switch.controle_chaudiere/switch.turn_off`\n- 将锅炉编程至 25°,从而强制其点火:`climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- 向锅炉发送 10°,从而强制其熄灭:`climate.thermostat_chaudiere/climate.set_temperature/temperature:10`", + "data": { + "central_boiler_activation_service": "开启命令", + "central_boiler_deactivation_service": "关闭命令", + "central_boiler_activation_delay_sec": "激活延迟(秒)", + "keep_alive_boiler_delay_sec": "保活延迟(秒)" + }, + "data_description": { + "central_boiler_activation_service": "开启中央锅炉的命令,格式类似 entity_id/service_name[/attribute:value]", + "central_boiler_deactivation_service": "关闭中央锅炉的命令,格式类似 entity_id/service_name[/attribute:value]", + "central_boiler_activation_delay_sec": "当需要打开中央锅炉时,在激活中央锅炉之前延迟几秒", + "keep_alive_boiler_delay_sec": "如果启用了保持活动状态,则定期重新发送锅炉命令的延迟(以秒为单位)。设置为 0 以禁用保持活动状态。" + } + }, + "valve_regulation": { + "title": "带阀自调节 - {name}", + "description": "通过直接控制阀门进行自我调节的配置", + "data": { + "offset_calibration_entity_ids": "偏移校准实体", + "opening_degree_entity_ids": "开度实体", + "closing_degree_entity_ids": "闭合度实体", + "proportional_function": "算法", + "opening_threshold_degree": "开启阈值度数", + "min_opening_degrees": "最小开度", + "max_opening_degrees": "最大开度", + "max_closing_degree": "最大关闭度" + }, + "data_description": { + "offset_calibration_entity_ids": "“偏移校准”实体列表。如果你的 TRV 提供该实体,请设置它以获得更好的调节效果。每个底层 climate 实体应对应一个。", + "opening_degree_entity_ids": "“开度”实体列表。每个底层 climate 实体应对应一个。", + "closing_degree_entity_ids": "“闭合度”实体列表。如果你的 TRV 提供该实体,请设置它以获得更好的调节效果。每个底层 climate 实体应对应一个。", + "proportional_function": "要使用的算法(其他算法可以作为{plugins_link}安装)", + "opening_threshold_degree": "阀门开度低于此值时应视为关闭(并应用 max_closing_degree)", + "min_opening_degrees": "当超过先前阈值时每个底层设备的开度最小值,以逗号分隔。默认为 0。示例:20,25,30", + "max_opening_degrees": "开度最大值。阀门的开度永远不会超过该值", + "max_closing_degree": "关闭度最大值。阀门永远不会在高于该值时关闭。将其设置为 100 以完全关闭阀门" + } + }, + "sync_device_internal_temp": { + "title": "同步设备内部温度", + "description": "配置以将底层设备的内部温度与所选实体同步", + "data": { + "sync_with_calibration": "应用偏移校准", + "sync_entity_ids": "用于同步的实体 ID" + }, + "data_description": { + "sync_with_calibration": "勾选以在同步内部温度时应用偏移校准。取消选中可直接复制所选实体的温度", + "sync_entity_ids": "用于同步底层设备内部温度的实体列表。每个底层设备应该有一个。如果选中复选框,则应为校准实体,否则应为温度实体。两者都是 `number` 实体。" + } + }, + "auto_tpi_configuration": { + "title": "自动 TPI - 配置 - {name}", + "description": "自动 TPI 学习的常规设置。", + "data": { + "auto_tpi_learning_type": "学习类型", + "auto_tpi_heating_rate": "升温速率({unit}/h)", + "heater_heating_time": "加热时间(分钟)", + "heater_cooling_time": "冷却时间(分钟)", + "auto_tpi_aggressiveness": "激进程度", + "auto_tpi_enable_advanced_settings": "启用高级参数", + "auto_tpi_continuous_kext": "持续学习 Kext" + }, + "data_description": { + "auto_tpi_learning_type": "选择“发现”用于初始启动(加权平均,权重 1),或选择“微调”用于持续调整(EWMA,Alpha 0.08)。", + "auto_tpi_heating_rate": "散热器升温能力({unit}每小时)。保留为 0 以进行自动学习(引导程序)。", + "heater_heating_time": "达到满功率的时间(分钟)", + "heater_cooling_time": "停止后的冷却时间(分钟)\n\n| 类型 | 加热时间 | 冷却时间 |\n| :--- | :--- | :--- |\n| 电暖器 | 5mn | 7mn |\n| 水暖散热器 | 15mn | 20mn |\n| 地暖 | 30mn | 45mn |", + "auto_tpi_aggressiveness": "学习系数的缩减因子 (50-100%)。较低的值会产生更保守的系数并降低超调风险。", + "auto_tpi_enable_advanced_settings": "勾选后可访问参数微调(Alpha、衰减、权重)。", + "auto_tpi_continuous_kext": "选中以启用 AutoTPI 会话之外的外部系数 (Kext) 的持续学习。" + } + }, + "auto_tpi_avg_settings": { + "title": "自动 TPI - 加权平均 - {name}", + "description": "加权平均法参数。", + "data": { + "auto_tpi_avg_initial_weight": "初始权重" + }, + "data_description": { + "auto_tpi_avg_initial_weight": "平均计算中现有系数的初始权重(典型值为 1-50)。权重越高,学习越慢也越稳定。" + } + }, + "auto_tpi_ema_settings": { + "title": "Auto TPI - EWMA - {name}", + "description": "EWMA 方法的参数。\n\n建议:\n| 场景 | Alpha (ema_alpha) | 衰减率 (ema_decay_rate) |\n| :--- | :--- | :--- |\n| 强力调整 | 0.15 | 0.08 |\n| 精细调节 | 0.08 | 0.12 |", + "data": { + "auto_tpi_ema_alpha": "Alpha", + "auto_tpi_ema_decay_rate": "衰减率", + "auto_tpi_continuous_kext_alpha": "连续 Kext Alpha" + }, + "data_description": { + "auto_tpi_ema_alpha": "平滑因子 (0-1)。更高=更快的适应", + "auto_tpi_ema_decay_rate": "Alpha 随时间下降的速率(稳定)", + "auto_tpi_continuous_kext_alpha": "用于连续 Kext 学习的平滑因子 (Alpha)。默认 0.04(大约 3-5 天适应)。" + } + }, + "heating_failure_detection": { + "title": "加热故障检测 - {name}", + "description": "配置加热故障检测功能。当预期加热但温度不升高时,或者当加热关闭但温度持续升高时,该功能会检测到异常情况。", + "data": { + "use_heating_failure_detection_feature": "启用加热故障检测", + "use_heating_failure_detection_central_config": "使用中央加热故障检测配置", + "heating_failure_threshold": "加热故障阈值", + "cooling_failure_threshold": "冷却故障阈值", + "heating_failure_detection_delay": "检测延迟(分钟)", + "temperature_change_tolerance": "温度变化容限 (°C)", + "failure_detection_enable_template": "检测启用模板" + }, + "data_description": { + "use_heating_failure_detection_feature": "使用 TPI 启用 VTherms 加热异常检测", + "use_heating_failure_detection_central_config": "勾选使用中央加热故障检测配置。取消选中为此 VTherm 使用特定参数", + "heating_failure_threshold": "开启百分比阈值,高于该阈值加热将导致温度升高 (0.9 = 90%)", + "cooling_failure_threshold": "开启百分比阈值,低于该阈值温度不应升高 (0.0 = 0%)", + "heating_failure_detection_delay": "检查温度是否按预期变化前需要等待的时间(分钟)", + "temperature_change_tolerance": "考虑显著的最小温度变化度。忽略较小的变化以过滤传感器噪声(默认值:0.5°C)", + "failure_detection_enable_template": "可选的 Jinja2 模板,必须返回 True 才能启用检测。当外部热源处于活动状态时(例如,双花括号中的 `is_state('binary_sensor.wood_stove', 'off')`),可用于暂时禁用检测。如果为空,则始终启用检测。" + } + } + }, + "error": { + "unknown": "意外错误", + "unknown_entity": "未知实体 ID", + "window_open_detection_method": "只能使用一种开窗检测方法:使用窗户传感器,或通过温度阈值自动检测,不能同时使用。", + "no_central_config": "您无法选中“使用中央配置”,因为未找到中央配置。您需要创建一个“中央配置”类型的多功能恒温器才能使用它。", + "service_configuration_format": "服务配置格式错误", + "valve_regulation_nb_entities_incorrect": "阀门调节的阀门实体数量应等于底层设备数量", + "sync_device_internal_temp_nb_entities_incorrect": "同步设备内部温度的实体数量应等于底层数量", + "min_opening_degrees_format": "需要逗号分隔的正整数列表。示例:20,25,30", + "max_opening_degrees_format": "需要一个介于 0 和阀门最大值之间的逗号分隔的正整数列表。示例:80,85,90", + "min_max_opening_degrees_inconsistent": "对于每个底层设备,max_opening_degrees 必须严格大于 min_opening_degrees。请检查你的配置。", + "vswitch_configuration_incorrect": "命令自定义配置不正确。对于不是 switch 的底层实体,这是必需的,格式必须为 service_name[/attribute:value]。更多信息请参阅 README。" + }, + "abort": { + "already_configured": "设备已配置" + } + }, + "selector": { + "thermostat_type": { + "options": { + "thermostat_central_config": "集中配置", + "thermostat_over_switch": "基于开关的恒温器", + "thermostat_over_climate": "基于 climate 的恒温器", + "thermostat_over_valve": "基于阀门的恒温器" + } + }, + "auto_regulation_mode": { + "options": { + "auto_regulation_slow": "慢", + "auto_regulation_strong": "强", + "auto_regulation_medium": "中等", + "auto_regulation_light": "轻度", + "auto_regulation_expert": "专家", + "auto_regulation_none": "无自动调节", + "auto_regulation_valve": "阀门直接控制" + } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "无自动风扇", + "auto_fan_low": "低", + "auto_fan_medium": "中速", + "auto_fan_high": "高", + "auto_fan_turbo": "涡轮" + } + }, + "window_action": { + "options": { + "window_turn_off": "关闭", + "window_fan_only": "仅风扇", + "window_frost_temp": "防冻保护", + "window_eco_temp": "生态" + } + }, + "presets": { + "options": { + "frost": "防冻保护", + "eco": "生态", + "comfort": "舒适", + "boost": "增强" + } + }, + "auto_start_stop": { + "options": { + "auto_start_stop_none": "无自动启停", + "auto_start_stop_very_slow": "检测速度非常慢", + "auto_start_stop_slow": "慢速检测", + "auto_start_stop_medium": "中等速度检测", + "auto_start_stop_fast": "快速检测" + } + }, + "auto_tpi_calculation_method": { + "options": { + "average": "平均值", + "ema": "指数移动平均线 (EMA)" + } + }, + "auto_tpi_learning_type": { + "options": { + "discovery": "发现", + "fine_tuning": "微调" + } + } + }, + "entity": { + "climate": { + "versatile_thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "power": "功率限制", + "safety": "安全", + "none": "手动", + "frost": "防冻" + } + } + } + } + }, + "binary_sensor": { + "safety_state": { + "name": "安全状态" + }, + "heating_failure_state": { + "name": "加热故障状态" + }, + "overpowering_state": { + "name": "过载状态" + }, + "window_state": { + "name": "窗户状态" + }, + "motion_state": { + "name": "运动状态" + }, + "presence_state": { + "name": "在家状态" + }, + "window_bypass_state": { + "name": "窗户旁路" + }, + "central_boiler_state": { + "name": "中央锅炉" + } + }, + "number": { + "frost_temp": { + "name": "防冻" + }, + "eco_temp": { + "name": "生态" + }, + "comfort_temp": { + "name": "舒适" + }, + "boost_temp": { + "name": "增强" + }, + "frost_ac_temp": { + "name": "防冻空调" + }, + "eco_ac_temp": { + "name": "节能空调" + }, + "comfort_ac_temp": { + "name": "舒适空调" + }, + "boost_ac_temp": { + "name": "增强空调" + }, + "frost_away_temp": { + "name": "离家防冻" + }, + "eco_away_temp": { + "name": "离家节能" + }, + "comfort_away_temp": { + "name": "离家舒适" + }, + "boost_away_temp": { + "name": "离家增强" + }, + "eco_ac_away_temp": { + "name": "离家节能空调" + }, + "comfort_ac_away_temp": { + "name": "离家舒适空调" + }, + "boost_ac_away_temp": { + "name": "离家增强空调" + } + } + }, + "services": { + "set_auto_tpi_mode": { + "name": "设置自动 TPI 模式", + "description": "启用或禁用自动 TPI 学习模式", + "fields": { + "auto_tpi_mode": { + "name": "自动 TPI 模式", + "description": "启用(true)或禁用(false)自动 TPI 学习" + }, + "reinitialise": { + "name": "重置学习数据", + "description": "启用自动 TPI 模式时重置所有学习数据(默认为 true)" + } + } + }, + "reset_auto_tpi_capacities": { + "name": "重置 Auto TPI 能力", + "description": "将学习的最大加热和冷却能力重置为 1.0 °C/h。学习的系数不会重置。" + }, + "auto_tpi_calibrate_capacity": { + "name": "Auto TPI 校准能力", + "description": "使用实体历史数据的线性回归来校准加热/冷却能力(以°C/小时为单位)。学习的系数不会更新。", + "fields": { + "start_date": { + "name": "历史开始日期", + "description": "检索历史记录的日期(默认为 30 天前)" + }, + "end_date": { + "name": "历史结束日期", + "description": "检索历史记录的截止日期(默认为现在)" + }, + "hvac_mode": { + "name": "HVAC 模式", + "description": "校准哪种模式(加热或冷却)。" + }, + "save_to_config": { + "name": "保存到配置", + "description": "将计算出的容量应用于自动 TPI 配置。" + }, + "min_power_threshold": { + "name": "最小功率阈值", + "description": "将周期视为饱和的最小功率百分比(默认 95%)。较低的值将包括更多的周期,但可能不太准确。" + } + } + }, + "set_tpi_parameters": { + "name": "设置 TPI 参数", + "description": "更改 TPI 参数", + "fields": { + "minimal_activation_delay": { + "name": "最小激活延迟", + "description": "设备不会被激活的延迟秒数" + }, + "minimal_deactivation_delay": { + "name": "最小停用延迟", + "description": "设备保持活动状态的延迟(以秒为单位)" + }, + "tpi_threshold_low": { + "name": "TPI 阈值低", + "description": "以°为单位的阈值,在该阈值下,TPI 算法将开启。 0表示无阈值" + }, + "tpi_threshold_high": { + "name": "TPI 高阈值", + "description": "以°为单位的阈值,高于该阈值,TPI 算法将关闭。 0表示无阈值" + } + } + }, + "lock": { + "name": "锁定", + "description": "锁定恒温器,防止 UI 或自动化对其配置进行任何更改。", + "fields": { + "code": { + "name": "锁码", + "description": "可选锁码" + } + } + }, + "unlock": { + "name": "解锁", + "description": "解锁恒温器,允许更改其配置。", + "fields": { + "code": { + "name": "锁码", + "description": "可选锁码" + } + } + }, + "download_logs": { + "name": "下载日志", + "description": "收集并下载 VTherm 实体的过滤日志。", + "fields": { + "log_level": { + "name": "日志级别", + "description": "要包含的最低日志级别" + }, + "period_start": { + "name": "周期开始", + "description": "提取周期开始时间" + }, + "period_end": { + "name": "周期结束", + "description": "提取周期结束时间" + } + } + } + }, + "exceptions": { + "auto_tpi_learning_stopped": { + "message": "由于连续 3 次失败,{name} 的自动 TPI 学习已停止。原因:{reason}。请检查您的配置。" + } + } +} diff --git a/custom_components/versatile_thermostat/underlying_state_manager.py b/custom_components/versatile_thermostat/underlying_state_manager.py new file mode 100644 index 0000000..65e3087 --- /dev/null +++ b/custom_components/versatile_thermostat/underlying_state_manager.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# pylint: disable=broad-exception-caught + +"""Underlying state manager for versatile_thermostat. + +This module provides `UnderlyingStateManager` which listens to state change +events for a list of entity_ids and stores the last known `State` for each +entity. +""" +from typing import List, Optional, Callable, Any +import logging +from vtherm_api.log_collector import get_vtherm_logger + +from homeassistant.core import HomeAssistant, State +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.helpers.event import async_track_state_change_event + +_LOGGER = get_vtherm_logger(__name__) + + +class UnknownEntity(Exception): + """Raised when an entity_id is not managed by the manager.""" + + +class UnderlyingStateManager: + """Manage states of underlying entities. + + Accepts a list of `entity_id` strings to monitor, initializes an + internal array of states (`State | None`) and stores the last received + state for each entity when state change events occur. + + Optionally accepts `on_change` callback with signature + `async def on_change(entity_id: str, new_state: Optional[State])` that + will be scheduled when a state change is received. + """ + + def __init__(self, hass: HomeAssistant, on_change: Optional[Callable[[str, Optional[State], Optional[State]], Any]] = None) -> None: + """Create the manager without any monitored entity IDs. + + Entities can be added later with `add_underlying_entities`. + """ + self._hass: HomeAssistant = hass + self._entity_ids: List[str] = [] + self._on_change = on_change + + # no states initially; entities added at runtime + self._states: List[Optional[State]] = [] + + # no listener registered until entities are added + self._remove_callback: Optional[Callable[[], None]] = None + + async def _state_changed(self, event) -> None: + """Internal callback invoked on each state change event.""" + new_state: Optional[State] = event.data.get("new_state", None) + old_state: Optional[State] = event.data.get("old_state", None) + # Retrieve the entity_id from the event (sometimes in data or via new_state) + entity_id = event.data.get("entity_id") if event.data.get("entity_id") else ( + new_state.entity_id if new_state else None + ) + _LOGGER.debug( + "UnderlyingStateManager - State change event received: %s for entity_id: %s", + new_state, + entity_id, + ) + if entity_id is None: + return + + if not self._set_state(entity_id, new_state): + return + + if self._on_change: + try: + self._hass.async_create_task(self._on_change(entity_id, new_state, old_state)) + except Exception as err: # pragma: no cover - defensive + _LOGGER.exception("Error %s scheduling on_change for %s", err, entity_id) + + def get_state(self, entity_id: str) -> Optional[State]: + """Return the last known `State` for `entity_id`, or `None` if unknown.""" + idx = self._index_of(entity_id) + if idx is None: + state = self._hass.states.get(entity_id) + if state is not None: + _LOGGER.debug( + "UnderlyingStateManager - Requested state for unknown entity_id: %s, found in HA", + entity_id, + ) + return state + _LOGGER.error("UnderlyingStateManager - Requested state for unknown entity_id: %s", entity_id) + return None + return self._states[idx] + + def _set_state(self, entity_id: str, state: Optional[State]) -> bool: + """Set the cached state for an entity and schedule `on_change` if present. + Return True if the state was updated, False if ignored. + States that are `None`, `STATE_UNAVAILABLE`, or `STATE_UNKNOWN` are ignored + """ + idx = self._index_of(entity_id) + if idx is None: + raise UnknownEntity(f"Entity ID {entity_id} is not managed by UnderlyingStateManager") + + # if state is None or state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN, None]: + # _LOGGER.debug("UnderlyingStateManager - Ignoring state change to unavailable/unknown for entity_id: %s", entity_id) + # return False + + self._states[idx] = state + return True + + @property + def is_all_states_initialized(self) -> bool: + """Return True if all monitored states are initialized and available. + + A state is considered initialized if it is not None and its + `.state` is not `STATE_UNAVAILABLE` or `STATE_UNKNOWN`. + """ + for st in self._states: + if st is None or st.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]: + return False + return len(self._states) == len(self._entity_ids) + + def get_uninitialized_entities(self) -> List[str]: + """Return a list of entity_ids that are not initialized.""" + uninitialized = [] + for entity_id, state in zip(self._entity_ids, self._states): + if state is None or state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]: + uninitialized.append(entity_id) + return uninitialized + + def _index_of(self, entity_id: str) -> Optional[int]: + """Return the index of `entity_id` in the monitored list, or None.""" + try: + return self._entity_ids.index(entity_id) + except ValueError: + return None + + def add_underlying_entities(self, entity_ids: List[str]) -> None: + """Add multiple entity_ids to the monitored list at runtime. + + Existing entities are ignored. The state change listener is (re) + registered for the updated entity list. If any added entity has an + initial state and `on_change` is set, the callback is scheduled for + that entity. + """ + added = [] + for entity_id in entity_ids: + if entity_id in self._entity_ids: + continue + self._entity_ids.append(entity_id) + state = self._hass.states.get(entity_id) + # should always add an initial state even if None + self._states.append(state) + if state and state.state not in [STATE_UNAVAILABLE, STATE_UNKNOWN, None]: + # but don't notify if not a real state + added.append(entity_id) + + # re-register the state change listener for the new set of entity_ids + if self._remove_callback: + try: + self._remove_callback() + except Exception: + _LOGGER.exception("Error removing previous callback while adding %s", added) + + self._remove_callback = async_track_state_change_event( + self._hass, self._entity_ids, self._state_changed + ) + + # schedule on_change for each newly added entity if initial state exists + if self._on_change: + for eid in added: + st = self._states[self._entity_ids.index(eid)] + if st is not None: + try: + self._hass.async_create_task(self._on_change(eid, st, None)) + except Exception as error: # pragma: no cover - defensive + _LOGGER.exception("Error scheduling on_change for %s. error is %s", eid, error) + + def stop(self) -> None: + """Stop listening to state changes and remove the callback.""" + if self._remove_callback: + try: + self._remove_callback() + except Exception: + pass + self._remove_callback = None diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py new file mode 100644 index 0000000..868f801 --- /dev/null +++ b/custom_components/versatile_thermostat/underlyings.py @@ -0,0 +1,1565 @@ +# pylint: disable=unused-argument, line-too-long, too-many-lines, broad-exception-caught + +"""Underlying entities classes""" +import re +from typing import Any, Dict, List, Optional, Tuple, TypeVar +from collections.abc import Callable +from datetime import datetime, timedelta + +from enum import StrEnum + +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfTemperature +from homeassistant.core import State + +from homeassistant.exceptions import ServiceNotFound + +from homeassistant.core import HomeAssistant, CALLBACK_TYPE, Context, ServiceResponse +from homeassistant.components.climate import ( + ClimateEntityFeature, + DOMAIN as CLIMATE_DOMAIN, + HVACAction, + HVACMode, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HUMIDITY, + SERVICE_SET_SWING_MODE, + SERVICE_SET_SWING_HORIZONTAL_MODE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_SET_TEMPERATURE, +) + +from homeassistant.components.number import SERVICE_SET_VALUE + +from homeassistant.helpers.event import async_call_later +from homeassistant.util import dt as dt_util + +from vtherm_api.log_collector import get_vtherm_logger +from .opening_degree_algorithm import OpeningClosingDegreeCalculation + + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .vtherm_hvac_mode import VThermHvacMode, from_ha_hvac_mode, to_legacy_ha_hvac_mode +from .keep_alive import IntervalCaller +from .vtherm_central_api import VersatileThermostatAPI +from .underlying_state_manager import UnderlyingStateManager + +resend_delay_sec = 2 + +_LOGGER = get_vtherm_logger(__name__) + +class UnderlyingEntityType(StrEnum): + """All underlying device type""" + + # A switch + SWITCH = "switch" + + # a climate + CLIMATE = "climate" + + # a valve + VALVE = "valve" + + # a direct valve regulation + VALVE_REGULATION = "valve_regulation" + +# ---------------------------------------------------------------- +# UnderlyingEntity +# ---------------------------------------------------------------- +class UnderlyingEntity: + """Represent a underlying device which could be a switch or a climate""" + + def __init__( + self, + hass: HomeAssistant, + thermostat: Any, + entity_type: UnderlyingEntityType, + entity_id: str, + ) -> None: + """Initialize the underlying entity""" + self._hass: HomeAssistant = hass + self._thermostat: Any = thermostat + self._type: UnderlyingEntityType = entity_type + self._entity_id: str = entity_id + self._power_reservation_key = entity_id + self._hvac_mode: VThermHvacMode | None = None + self._on_cycle_start_callbacks: list[Callable] = [] + self._last_command_sent_datetime: datetime = dt_util.utc_from_timestamp(0) + # Use UnderlyingStateManager to track underlying entity state + self._state_manager: UnderlyingStateManager = UnderlyingStateManager(self._hass, on_change=self._underlying_changed) + self._is_initialized: bool = False + self._api = VersatileThermostatAPI.get_vtherm_api(hass) + + def register_cycle_callback(self, on_start: Callable): + """Register a callback for cycle start""" + self._on_cycle_start_callbacks.append(on_start) + + def __str__(self): + return str(self._thermostat) + "-" + self._entity_id + + @property + def entity_id(self): + """The entity id represented by this class""" + return self._entity_id + + @property + def power_reservation_key(self) -> str: + """Return the stable power reservation key for this underlying.""" + return self._power_reservation_key + + @property + def entity_type(self) -> UnderlyingEntityType: + """The entity type represented by this class""" + return self._type + + @property + def is_initialized(self) -> bool: + """True if the underlying is initialized and have received a non None state""" + return self._is_initialized + + def startup(self): + """Startup the Entity. Listen to the underlying state changes""" + # starts listening and can provide the initial cached state. + self._state_manager.add_underlying_entities([self._entity_id]) + + async def _underlying_changed(self, entity_id: str, new_state: Optional[State], old_state: Optional[State] = None): + """Handle underlying state change notified by UnderlyingStateManager. + + `new_state` may be None when the entity is removed/unavailable. + Runs the initial state checks when all underlying entities are initialized. + """ + _LOGGER.debug("%s - --------> Underlying state change received: '%s'", self, new_state) + + # If not yet initialized and we received a valid initial state, run initial checks + if not self.is_initialized: + # Check if we have a valid state for all underlying entities for the first time + if self._state_manager.is_all_states_initialized: + self._is_initialized = True + _LOGGER.debug("%s - All underlying states are now initialized", self) + await self.check_initial_state() + # if all underlying of the vtherm are initialized, notify the parent thermostat + if self._thermostat.is_initialized: + await self._thermostat.init_underlyings_completed(self._entity_id) + return + else: + _LOGGER.debug("%s - Underlying state still not yet initialized", self) + # Otherwise, the manager holds the latest state + # Update the hvac_action of the parent which could have changed + if self._thermostat.hvac_action != self._thermostat.calculate_hvac_action(): + self._thermostat.update_custom_attributes() + self._thermostat.async_write_ha_state() + + # update the sensor which count the number of active devices is vtherm is used by central boiler + if ( + self._thermostat.is_used_by_central_boiler + and self._api.central_boiler_manager is not None + and self._api.central_boiler_manager.nb_device_active_for_boiler_entity is not None + ): + await self._api.central_boiler_manager.nb_device_active_for_boiler_entity.calculate_nb_active_devices(None) + + async def set_hvac_mode(self, hvac_mode: VThermHvacMode): + """Set the HVACmode""" + self._hvac_mode = hvac_mode + return + + async def set_temperature(self, temperature, max_temp, min_temp): + """Set the target temperature""" + return + + # This should be the correct way to handle turn_off and turn_on but this breaks the unit test + # will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression + async def turn_off(self): + """Turn off the underlying equipement. + Need to be overriden""" + return NotImplementedError + + async def turn_on(self): + """Turn off the underlying equipement. + Need to be overriden""" + return NotImplementedError + + def remove_entity(self): + """Remove the underlying entity""" + self._on_cycle_start_callbacks.clear() + # Stop the state manager listener for this entity + self._state_manager.stop() + + async def check_initial_state(self): + """Prevent the underlying to be on but thermostat is off""" + return NotImplementedError + + # override to be able to mock the call + def call_later( + self, hass: HomeAssistant, delay_sec: int, called_method + ) -> CALLBACK_TYPE: + """Call the method after a delay""" + return async_call_later(hass, delay_sec, called_method) + + async def hass_services_async_call( + self, + domain: str, + service: str, + service_data: dict[str, Any] | None = None, + blocking: bool = False, + context: Context | None = None, + target: dict[str, Any] | None = None, + return_response: bool = False, + ) -> ServiceResponse: + """Wrapper for HASS service calls""" + try: + response: ServiceResponse = await self._hass.services.async_call(domain, service, service_data, blocking, context, target, return_response) + + self._last_command_sent_datetime = self._thermostat.now + return response + except Exception as err: + _LOGGER.error("%s - Error calling service %s.%s: %s. The underlying will not change its state.", self, domain, service, err) + + def clamp_sent_value(self, value) -> float: + """capping of the value send to the underlying eqt""" + return value + + async def check_overpowering(self) -> bool: + """Check that a underlying can be turned on, else + activate the overpowering state of the VTherm associated. + Returns True if the check is ok (no overpowering needed)""" + ret, _ = await self._thermostat.power_manager.check_power_available( + self.power_reservation_key + ) + if not ret: + _LOGGER.debug("%s - overpowering is detected", self) + await self._thermostat.power_manager.set_overpowering(True) + return False + + return True + + async def send_value_to_number(self, number_entity_id: str, value: int): + """Send a value to a number entity""" + try: + 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, + service_data=data, + target=target, + ) + except ServiceNotFound as err: + _LOGGER.error(err) + # This could happens in unit test if input_number domain is not yet loaded + # raise err + + @property + def hvac_mode(self) -> VThermHvacMode | None: + """Return the current hvac_mode""" + return self._hvac_mode + + @property + def should_device_be_active(self) -> bool | None: + """If the underlying device should currently be active. + Need to be overriden""" + return NotImplementedError + + @property + def is_device_active(self) -> bool | None: + """If the underlying device is currently active. + Need to be overriden""" + return NotImplementedError + + @property + def hvac_action(self) -> HVACAction: + """Calculate a hvac_action""" + raise NotImplementedError + + @property + def is_inversed(self): + """Tells if the switch command should be inversed""" + return False + + @property + def state_manager(self) -> UnderlyingStateManager: + """Return the underlying state manager""" + return self._state_manager + + # For testing compatibility + @property + def _last_known_underlying_state(self) -> Optional[State]: + """Return the last known underlying state""" + return self._state_manager.get_state(self._entity_id) + + @property + def last_change(self) -> datetime | None: + """Return the last_changed datetime of the underlying device if known.""" + state = self._state_manager.get_state(self._entity_id) + return state.last_changed if state else None + + async def check_and_repair(self) -> bool: + """Check if the underlying device state matches the desired state and repair if needed. + Returns True if a repair was performed.""" + should_be_active = self.should_device_be_active + is_active = self.is_device_active + + _LOGGER.debug("%s - Checking if underlying state needs repair. should_be_on=%s, is_on=%s", self, should_be_active, is_active) + + if should_be_active is None or is_active is None: + return False + + if should_be_active == is_active: + return False + + if should_be_active: + await self.turn_on() + else: + await self.turn_off() + return True + + +# ---------------------------------------------------------------- +# UnderlyingSwitch +# ---------------------------------------------------------------- +class UnderlyingSwitch(UnderlyingEntity): + """Represent a underlying switch""" + + def __init__( + self, hass: HomeAssistant, thermostat: Any, switch_entity_id: str, keep_alive_sec: float, vswitch_on: str = None, vswitch_off: str = None + ) -> None: + """Initialize the underlying switch""" + + super().__init__( + hass=hass, + thermostat=thermostat, + entity_type=UnderlyingEntityType.SWITCH, + entity_id=switch_entity_id, + ) + self._on_time_sec = 0 + self._off_time_sec = 0 + self._new_on_time_sec = 0 + self._new_off_time_sec = 0 + self._is_removed = False + self._keep_alive = IntervalCaller(hass, keep_alive_sec) + self._vswitch_on = vswitch_on.strip() if vswitch_on else None + self._vswitch_off = vswitch_off.strip() if vswitch_off else None + self._domain = self._entity_id.split(".")[0] + # build command + command, data, state_on = self.build_command(use_on=True) + self._on_command = {"command": command, "data": data, "state": state_on} + command, data, state_off = self.build_command(use_on=False) + self._off_command = {"command": command, "data": data, "state": state_off} + + # the underlying is cycling with on_percent > 0 + self._should_be_on = False + + # true if the on part of the cycle is running. Off if the off part is running + self._is_on_part_running = False + + @overrides + @property + def is_inversed(self) -> bool: + """Tells if the switch command should be inversed""" + return self._thermostat.is_inversed + + @property + def keep_alive_sec(self) -> float: + """Return the switch keep-alive interval in seconds.""" + return self._keep_alive.interval_sec + + @overrides + def startup(self): + super().startup() + self._keep_alive.set_async_action(self._keep_alive_callback) + + # @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression + async def set_hvac_mode(self, hvac_mode: VThermHvacMode) -> bool: + """Set the HVACmode. Returns true if something have change""" + + if hvac_mode == VThermHvacMode_OFF: + if self.is_device_active: + await self.turn_off() + + if self.hvac_mode != hvac_mode: + await super().set_hvac_mode(hvac_mode) + self._calculate_should_be_on() + return True + else: + return False + + def _calculate_should_be_on(self, should_be_on: bool | None = None) -> bool: + """Calculate and update the _should_be_on flag""" + self._should_be_on = self._hvac_mode in [VThermHvacMode_HEAT, VThermHvacMode_COOL] and self._on_time_sec > 0 + + return self._should_be_on + + @property + def should_device_be_active(self) -> bool: + """If the toggleable device is currently active.""" + return self._calculate_should_be_on() + + @property + def is_device_active(self) -> bool | None: + """If the toggleable device is currently active.""" + if not self.is_initialized: + return None + state = self._state_manager.get_state(self._entity_id) + if state is None: + return None + + is_on = state.state == self._on_command.get("state") + return is_on + + async def check_initial_state(self): + """Prevent the underlying to be on but thermostat is off""" + hvac_mode = self._thermostat.vtherm_hvac_mode + + if hvac_mode == VThermHvacMode_OFF and self.is_device_active: + _LOGGER.info( + "%s - The hvac mode is OFF, but the underlying device is ON. Turning off device %s", + self, + self._entity_id, + ) + await self.turn_off() + # elif hvac_mode != VThermHvacMode_OFF and not is_device_active: + # it is normal, the cycle could be started later or in off phase + + async def _keep_alive_callback(self): + """Keep alive: Turn on if already turned on, turn off if already turned off.""" + timer = self._keep_alive.backoff_timer + state: State | None = self._hass.states.get(self._entity_id) + # Normal, expected state.state values are "on" and "off". An absent + # underlying MQTT switch was observed to produce either state == None + # or state.state == STATE_UNAVAILABLE ("unavailable"). + if state is None or state.state == STATE_UNAVAILABLE: + if timer.is_ready(): + _LOGGER.warning( + "%s - Entity %s is not available (state: %s). Will keep trying " "keep alive calls, but won't log this condition every time.", + self, + self._entity_id, + state.state if state else "None", + ) + else: + if timer.in_progress: + timer.reset() + _LOGGER.warning( + "%s - Entity %s has recovered (state: %s).", + self, + self._entity_id, + state.state, + ) + await (self.turn_on() if self._is_on_part_running else self.turn_off()) + + def build_command(self, use_on: bool) -> Tuple[str, Dict[str, str]]: + """Build a command and returns a command and a dict as data""" + + value = None + data = {ATTR_ENTITY_ID: self._entity_id} + take_on = (use_on and not self.is_inversed) or (not use_on and self.is_inversed) + vswitch = self._vswitch_on if take_on else self._vswitch_off + if vswitch: + pattern = r"^(?P[^\s/]+)(?:/(?P[^\s:]+)(?::(?P[^\s]+))?)?$" + match = re.match(pattern, vswitch) + + if match: + # Extraire les groupes nommés + command = match.group("command") + argument = match.group("argument") + value = match.group("value") + if argument is not None and value is not None: + data.update({argument: value}) + else: + raise ValueError(f"Invalid input format: {vswitch}. Must be conform to 'command[/argument[:value]]'") + + else: + command = SERVICE_TURN_ON if take_on else SERVICE_TURN_OFF + + if value is None: + value = STATE_ON if take_on else STATE_OFF + + return command, data, value + + async def turn_off(self): + """Turn heater toggleable device off.""" + self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition + _LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id) + + command = self._off_command.get("command") + data = self._off_command.get("data") + + # This may fails if called after shutdown + try: + try: + self._thermostat.power_manager.sub_power_consumption_to_central_power_manager( + self.power_reservation_key + ) + _LOGGER.debug("%s - Sending command %s with data=%s", self, command, data) + await self._hass.services.async_call(self._domain, command, data) + self._is_on_part_running = False + self._keep_alive.set_async_action(self._keep_alive_callback) + except Exception: + self._keep_alive.cancel() + raise + except ServiceNotFound as err: + _LOGGER.error(err) + + async def turn_on(self): + """Turn heater toggleable device on.""" + self._keep_alive.cancel() # Cancel early to avoid a turn_on/turn_off race condition + _LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id) + + if not await self.check_overpowering(): + self._thermostat.requested_state.force_changed() + await self._thermostat.update_states(force=True) + + return False + + command = self._on_command.get("command") + data = self._on_command.get("data") + try: + try: + self._thermostat.power_manager.add_power_consumption_to_central_power_manager( + self.power_reservation_key + ) + _LOGGER.debug("%s - Sending command %s with data=%s", self, command, data) + await self._hass.services.async_call(self._domain, command, data) + self._is_on_part_running = True + self._keep_alive.set_async_action(self._keep_alive_callback) + return True + except Exception: + self._keep_alive.cancel() + raise + except ServiceNotFound as err: + _LOGGER.error(err) + + @overrides + def remove_entity(self): + """Remove the entity""" + self._is_removed = True + self._keep_alive.cancel() + super().remove_entity() + + def hvac_action(self) -> HVACAction: + """Calculate a hvac_action based on the current state and should_be_on""" + if not self.is_initialized: + return HVACAction.OFF + + return HVACAction.HEATING if self.should_device_be_active is True else HVACAction.OFF + + async def check_and_repair(self) -> bool: + """Check if the underlying device state matches the desired state and repair if needed. + Returns True if a repair was performed.""" + should_be_on = self._is_on_part_running + is_on = self.is_device_active + + _LOGGER.debug("%s - Checking if underlying switch state needs repair. should_be_on=%s, is_on=%s", self, should_be_on, is_on) + + if should_be_on is None or is_on is None: + return False + + if should_be_on == is_on: + return False + + if should_be_on: + await self.turn_on() + return True + else: + await self.turn_off() + return True + + +# ---------------------------------------------------------------- +# UnderlyingClimate +# ---------------------------------------------------------------- +class UnderlyingClimate(UnderlyingEntity): + """Represent a underlying climate""" + + def __init__( + self, + hass: HomeAssistant, + thermostat: Any, + climate_entity_id: str, + ) -> None: + """Initialize the underlying climate""" + + super().__init__( + hass=hass, + thermostat=thermostat, + entity_type=UnderlyingEntityType.CLIMATE, + entity_id=climate_entity_id, + ) + self._last_sent_temperature: Optional[float] = None + self._cancel_set_fan_mode_later: Optional[Callable[[], None]] = None + self._cancel_set_temperature_later: Optional[Callable[[], None]] = None + self._min_sync_entity: float = None + self._max_sync_entity: float = None + self._step_sync_entity: float = None + + async def set_hvac_mode(self, hvac_mode: VThermHvacMode) -> bool: + """Set the HVACmode of the underlying climate. Returns true if something have change""" + state = self._state_manager.get_state(self._entity_id) + if state is None: + return False + + if state.state == to_ha_hvac_mode(hvac_mode) and self._hvac_mode == hvac_mode: + _LOGGER.debug( + "%s - hvac_mode is already is requested state %s. Do not send any command", + self, + hvac_mode, + ) + return False + + # When turning on a climate, check that power is available + if hvac_mode in (VThermHvacMode_HEAT, VThermHvacMode_COOL) and not await self.check_overpowering(): + return False + + await super().set_hvac_mode(hvac_mode) + + data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": to_legacy_ha_hvac_mode(hvac_mode)} + await self.hass_services_async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + data, + ) + + # if restart the climate, then resend the target temperature 2 sec later for lazy SonoffTRVZB + if hvac_mode in (VThermHvacMode_HEAT, VThermHvacMode_COOL): + + async def callback_resend_temp(_): + await self.set_temperature(self._thermostat.target_temperature, None, None) + + if self._cancel_set_temperature_later: + self._cancel_set_temperature_later() + self._cancel_set_temperature_later = async_call_later(self._hass, resend_delay_sec, callback_resend_temp) + + return True + + @overrides + def remove_entity(self): + """Remove the entity""" + if self._cancel_set_fan_mode_later: + self._cancel_set_fan_mode_later() + self._cancel_set_fan_mode_later = None + if self._cancel_set_temperature_later: + self._cancel_set_temperature_later() + self._cancel_set_temperature_later = None + super().remove_entity() + + @property + def should_device_be_active(self): + """If the toggleable device is currently active.""" + return self.hvac_mode != VThermHvacMode_OFF and self.hvac_action not in [ + HVACAction.IDLE, + HVACAction.OFF, + None, + ] + + @property + def is_device_active(self): + """If the toggleable device is currently active.""" + state = self._state_manager.get_state(self._entity_id) + if state is None: + return None + + # Issue 1890 - unavailable/unknown entities should not be counted as active + if state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]: + return None + + # Issue 1779 - if no hvac_action is available use a fake hvac_action based on temperature + hvac_action = state.attributes.get("hvac_action", None) + if not hvac_action: + hvac_action = self.hvac_action + + # The device is active if hvac_mode is not OFF/IDLE and hvac_action is not OFF/IDLE. hvac_action could be None because it is not always implemented by all climate entities + return state.state != HVACMode.OFF and (hvac_action not in [HVACAction.IDLE, HVACAction.OFF]) + + async def check_initial_state(self): + """Prevent the underlying to be on but thermostat is off""" + underlying_state = self._state_manager.get_state(self._entity_id) + underlying_hvac_mode = from_ha_hvac_mode(underlying_state.state) if underlying_state else None + self._hvac_mode = underlying_hvac_mode + + is_device_active = underlying_state.state not in [HVACMode.OFF, STATE_UNAVAILABLE, STATE_UNKNOWN] + hvac_mode = self._thermostat.vtherm_hvac_mode + + if hvac_mode == VThermHvacMode_OFF and is_device_active: + _LOGGER.info( + "%s - The hvac mode is OFF, but the underlying device is ON. Turning off device %s", + self, + self._entity_id, + ) + await self.set_hvac_mode(hvac_mode) + elif hvac_mode != VThermHvacMode_OFF and not is_device_active: + _LOGGER.info( + "%s - The hvac mode is %s, but the underlying device is not ON. Turning on device %s if needed", + self, + hvac_mode, + self._entity_id, + ) + await self.set_hvac_mode(hvac_mode) + + async def _underlying_changed(self, entity_id: str, new_state: Optional[State], old_state: Optional[State] = None): + """Handle underlying state change notified by UnderlyingStateManager. + + `new_state` may be None when the entity is removed/unavailable. + Runs the initial state checks when all underlying entities are initialized. + """ + await super()._underlying_changed(entity_id, new_state, old_state) + if not self.is_initialized or new_state is None: + return + + # Check if one attributes has changed that could impact the thermostat + new_hvac_mode = VThermHvacMode(new_state.state) + old_hvac_mode = VThermHvacMode(old_state.state) if old_state else None + new_hvac_action = new_state.attributes.get("hvac_action") if new_state.attributes else None + old_hvac_action = old_state.attributes.get("hvac_action") if old_state and old_state.attributes else None + new_fan_mode = new_state.attributes.get("fan_mode") if new_state.attributes else None + new_target_temp = new_state.attributes.get("temperature") if new_state.attributes else None + + # Ignore new target temperature when out of range + if ( + not new_target_temp is None + and not self._thermostat.min_temp is None + and not self._thermostat.max_temp is None + and not (self._thermostat.min_temp <= new_target_temp <= self._thermostat.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._thermostat.min_temp, + self._thermostat.max_temp, + ) + new_target_temp = None + + under_temp_diff = 0 + if new_target_temp is not None: + last_sent_temperature = self.last_sent_temperature or 0 + under_temp_diff = new_target_temp - last_sent_temperature + + # check the dtemp is > step + step = self._thermostat.target_temperature_step or 1 + if -step < under_temp_diff < step: + under_temp_diff = 0 + new_target_temp = None + + # Forget event when the event holds no real changes + if new_hvac_mode == self._thermostat.hvac_mode: + new_hvac_mode = None + if new_hvac_action == old_hvac_action: + new_hvac_action = None + if new_fan_mode == self._thermostat.fan_mode: + new_fan_mode = None + if under_temp_diff == 0: + new_target_temp = None + + if new_hvac_mode is None and new_hvac_action is None and new_target_temp is None and new_fan_mode is None: + _LOGGER.debug( + "%s - a underlying state change event is received but no real change have been found. Forget the event", + self, + ) + return + + await self._thermostat.underlying_changed(self, new_hvac_mode, new_hvac_action, new_target_temp, new_fan_mode, new_state, old_state) + + async def set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + if not self.is_initialized: + return + + data = { + ATTR_ENTITY_ID: self._entity_id, + "fan_mode": fan_mode, + } + + if self._cancel_set_fan_mode_later: + self._cancel_set_fan_mode_later() + self._cancel_set_fan_mode_later = None + + delay: float = resend_delay_sec + now = self._thermostat.now + last_command_sent = self._last_command_sent_datetime + + if now > last_command_sent + timedelta(seconds=delay): + await self.hass_services_async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + data, + ) + + return + + # Add a delay if last command was sent less than delay seconds ago + # Some AC units (e.g. Daikin) do not handle multiple consecutive commands well + _LOGGER.debug( + "%s - #1458 - Delaying command set_fan_mode for underlying %s by %.2fs", + self, + self._entity_id, + delay, + ) + + async def callback_set_fan_mode(_): + await self.set_fan_mode(fan_mode) + + self._cancel_set_fan_mode_later = async_call_later(self._hass, delay, callback_set_fan_mode) + + async def set_humidity(self, humidity: int): + """Set new target humidity.""" + _LOGGER.info("%s - Set humidity: %s", self, humidity) + if not self.is_initialized: + return + data = { + ATTR_ENTITY_ID: self._entity_id, + "humidity": humidity, + } + + await self.hass_services_async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HUMIDITY, + data, + ) + + async def set_swing_mode(self, swing_mode): + """Set new target swing operation.""" + _LOGGER.info("%s - Set swing mode: %s", self, swing_mode) + if not self.is_initialized: + return + data = { + ATTR_ENTITY_ID: self._entity_id, + "swing_mode": swing_mode, + } + + await self.hass_services_async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + data, + ) + + async def set_swing_horizontal_mode(self, swing_horizontal_mode): + """Set new target swing horizontal operation.""" + _LOGGER.info("%s - Set swing horizontal mode: %s", self, swing_horizontal_mode) + if not self.is_initialized: + return + data = { + ATTR_ENTITY_ID: self._entity_id, + "swing_horizontal_mode": swing_horizontal_mode, + } + + await self.hass_services_async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_HORIZONTAL_MODE, + data, + ) + + async def set_temperature(self, temperature, max_temp, min_temp): + """Set the target temperature""" + if not self.is_initialized: + return + + # Issue 508 we have to take care of service set_temperature or set_range + target_temp = self.clamp_sent_value(temperature) + data = { + ATTR_ENTITY_ID: self._entity_id, + } + + _LOGGER.info("%s - Set setpoint temperature to: %s", self, target_temp) + + # Issue 807 add TARGET_TEMPERATURE only if in the features + if ClimateEntityFeature.TARGET_TEMPERATURE_RANGE in self.supported_features: + data.update( + { + "target_temp_high": target_temp, + "target_temp_low": target_temp, + } + ) + + if ClimateEntityFeature.TARGET_TEMPERATURE in self.supported_features: + data["temperature"] = target_temp + + try: + await self.hass_services_async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + data, + ) + except Exception as ex: + _LOGGER.error("%s - Error while sending set_temperature: %s", self, ex) + raise ex + + self._last_sent_temperature = target_temp + _LOGGER.debug("%s - Last_sent_temperature is now: %s", self, self._last_sent_temperature) + + @property + def last_sent_temperature(self) -> float | None: + """Get the last send temperature. None if no temperature have been sent yet""" + return self._last_sent_temperature + + @property + def hvac_action(self) -> HVACAction | None: + """Get the hvac action of the underlying""" + if not self.is_initialized: + return None + + hvac_action = self.underlying_hvac_action + if hvac_action is None: + # simulate hvac action if not provided by underlying climate + underlying_target = self.underlying_target_temperature + underlying_current = self.underlying_current_temperature + + if underlying_target is not None and underlying_current is not None: + # Both values come from the underlying device — use them as a consistent pair + target = underlying_target + current = underlying_current + else: + # Fall back entirely to VTherm-managed temperatures (room sensor) + # This prevents mixing device-internal and room sensor temperatures + target = self._thermostat.target_temperature + current = self._thermostat.current_temperature + hvac_mode = self.hvac_mode + + _LOGGER.debug( + "%s - hvac_action simulation target=%s, current=%s, hvac_mode=%s", + self, + target, + current, + hvac_mode, + ) + hvac_action = HVACAction.IDLE + if target is not None and current is not None: + dtemp = target - current + + if hvac_mode == VThermHvacMode_COOL and dtemp < 0: + hvac_action = HVACAction.COOLING + elif hvac_mode in [VThermHvacMode_HEAT, VThermHvacMode_HEAT_COOL] and dtemp > 0: + hvac_action = HVACAction.HEATING + + return hvac_action + + @property + def hvac_mode(self) -> VThermHvacMode | None: + """Get the hvac mode of the underlying""" + if not self.is_initialized: + return None + state = self._state_manager.get_state(self._entity_id) + return state.state if state is not None else None + + @property + def fan_mode(self) -> str | None: + """Get the fan_mode of the underlying""" + if not self.is_initialized or self.supported_features & ClimateEntityFeature.FAN_MODE == 0: + return None + return self.get_underlying_attribute("fan_mode") + + @property + def swing_mode(self) -> str | None: + """Get the swing_mode of the underlying""" + if not self.is_initialized or self.supported_features & ClimateEntityFeature.SWING_MODE == 0: + return None + return self.get_underlying_attribute("swing_mode") + + @property + def swing_horizontal_mode(self) -> str | None: + """Get the swing_horizontal_mode of the underlying""" + return self.get_underlying_attribute("swing_horizontal_mode") + + @property + def supported_features(self) -> ClimateEntityFeature: + """Get the supported features of the climate""" + return self.get_underlying_attribute("supported_features") or ClimateEntityFeature(0) + + @property + def hvac_modes(self) -> list[VThermHvacMode]: + """Get the hvac_modes""" + if not self.is_initialized: + return [] + return self.get_underlying_attribute("hvac_modes") + + @property + def current_humidity(self) -> float | None: + """Get the humidity""" + return self.get_underlying_attribute("current_humidity") + + @property + def fan_modes(self) -> list[str]: + """Get the fan_modes""" + if not self.is_initialized or self.supported_features & ClimateEntityFeature.FAN_MODE == 0: + return [] + return self.get_underlying_attribute("fan_modes") + + @property + def swing_modes(self) -> list[str]: + """Get the swing_modes""" + if not self.is_initialized or self.supported_features & ClimateEntityFeature.SWING_MODE == 0: + return [] + return self.get_underlying_attribute("swing_modes") + + @property + def swing_horizontal_modes(self) -> list[str]: + """Get the swing_horizontal_modes""" + if not self.is_initialized or self.supported_features & ClimateEntityFeature.SWING_HORIZONTAL_MODE == 0: + return [] + return self.get_underlying_attribute("swing_horizontal_modes") + + @property + def temperature_unit(self) -> str: + """Get the temperature_unit""" + return self.get_underlying_attribute("temperature_unit") or UnitOfTemperature.CELSIUS + + @property + def min_temp(self) -> str: + """Get the min_temp""" + return self.get_underlying_attribute("min_temp") + + @property + def max_temp(self) -> str: + """Get the max_temp""" + return self.get_underlying_attribute("max_temp") + + @property + def target_temperature_step(self) -> float: + """Get the target_temperature_step""" + return self.get_underlying_attribute("target_temperature_step") + + @property + def target_temperature_high(self) -> float: + """Get the target_temperature_high""" + return self.get_underlying_attribute("target_temperature_high") + + @property + def target_temperature_low(self) -> float: + """Get the target_temperature_low""" + return self.get_underlying_attribute("target_temperature_low") + + @property + def underlying_target_temperature(self) -> float: + """Get the target_temperature""" + value = self.get_underlying_attribute("target_temperature") + if value is not None: + return value + return self.get_underlying_attribute("temperature") + + @property + def underlying_current_temperature(self) -> float | None: + """Get the underlying current_temperature if it exists + and if initialized""" + return self.get_underlying_attribute("current_temperature") + + @property + def underlying_hvac_action(self) -> HVACAction | None: + """Get the underlying hvac_action if it exists + and if initialized""" + return self.get_underlying_attribute("hvac_action") + + @property + def is_aux_heat(self) -> bool: + """Get the is_aux_heat""" + return self.get_underlying_attribute("is_aux_heat") + + def get_underlying_attribute(self, attribute_name: str) -> Any: + """Get an attribute from the underlying climate""" + if not self.is_initialized: + return None + state = self._state_manager.get_state(self._entity_id) + if state is None: + return None + return state.attributes.get(attribute_name, None) + + @overrides + def clamp_sent_value(self, value) -> float: + """Try to adapt the target temp value to the min_temp / max_temp found + in the underlying entity (if any)""" + min_val = None + max_val = None + + if not self.is_initialized: + raise RuntimeError(f"{self} - cannot cap sent value because underlying is not initialized") + # return value + + # Gets the min_temp and max_temp. + # Values from state attributes are already in HA's configured unit (same as value), + # because HA's climate component normalizes them via show_temp() before storing. + # No unit conversion is needed here. + + if self.min_temp is not None: + min_val = self.min_temp + max_val = self.max_temp + + new_value = max(min_val, min(value, max_val)) + else: + _LOGGER.debug("%s - no min and max attributes on underlying", self) + new_value = value + + if new_value != value: + _LOGGER.info( + "%s - Target temp have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f", + self, + new_value, + value, + min_val, + max_val, + ) + + return new_value + + def set_min_max_step_sync_entity( + self, + min_sync_entity: float, + max_sync_entity: float, + step_sync_entity: float, + ): + """Set the min, max and step for the offset calibration synchronization""" + self._min_sync_entity = min_sync_entity + self._max_sync_entity = max_sync_entity + self._step_sync_entity = step_sync_entity + + async def check_and_repair(self) -> bool: + """Check if the underlying device state matches the desired state and repair if needed. + Returns True if a repair was performed.""" + hvac_mode = self._thermostat.vtherm_hvac_mode + + under_hvac_mode = self.hvac_mode + + _LOGGER.debug("%s - Checking if underlying climate state needs repair. hvac_mode=%s, under_hvac_mode=%s", self, hvac_mode, under_hvac_mode) + + if hvac_mode is None or under_hvac_mode is None: + return False + + if str(hvac_mode) == str(under_hvac_mode): + return False + + await self.set_hvac_mode(hvac_mode) + return True + + @property + def min_sync_entity(self) -> float: + """Get the min sync entity""" + return self._min_sync_entity + + @property + def max_sync_entity(self) -> float: + """Get the max sync entity""" + return self._max_sync_entity + + @property + def step_sync_entity(self) -> float: + """Get the step sync entity""" + return self._step_sync_entity + +# ---------------------------------------------------------------- +# UnderlyingValve +# ---------------------------------------------------------------- +class UnderlyingValve(UnderlyingEntity): + """Represent a underlying switch""" + + def __init__( + self, + hass: HomeAssistant, + thermostat: Any, + valve_entity_id: str, + entity_type: UnderlyingEntityType = UnderlyingEntityType.VALVE, + ) -> None: + """Initialize the underlying valve""" + + super().__init__( + hass=hass, + thermostat=thermostat, + entity_type=UnderlyingEntityType.VALVE, + entity_id=valve_entity_id, + ) + self._hvac_mode = None + self._percent_open: int | None = None # self._thermostat.valve_open_percent + self._min_open: float | None = None + self._max_open: float | None = None + self._last_sent_temperature = None + self._last_sent_opening_value: int | None = None + + def init_valve_state_min_max_open(self): + """Initialize the min and max open percent""" + if not self.is_initialized: + raise RuntimeError(f"{self} - cannot init min/max open because underlying is not initialized") + + valve_state = self._state_manager.get_state(self.entity_id) + valve_open: float = get_safe_float_value(valve_state.state) + if valve_open is None: + # should not happen + raise ValueError(f"{self} - cannot check_initial_state because underlying entity {self._entity_id} value {valve_state.state} is not a valid float") + + self._last_sent_opening_value = valve_open + + if "min" in valve_state.attributes and "max" in valve_state.attributes: + self._min_open = valve_state.attributes["min"] + self._max_open = valve_state.attributes["max"] + else: + self._min_open = 0 + self._max_open = 100 + + @overrides + async def check_initial_state(self): + """Handle initial valve state change to get the min and max open percent""" + # Initialize percent_open to current state + + self.init_valve_state_min_max_open() + + should_device_be_active = self.should_device_be_active + is_device_active = self.is_device_active + + if should_device_be_active and not is_device_active: + _LOGGER.info( + "%s - The valve should be active (percent_open=%.0f), but the underlying valve is closed (current_valve_opening=%.0f). Opening valve %s", + self, + self._percent_open or 9999, + self._last_sent_opening_value or 9999, + self._entity_id, + ) + await self.send_percent_open() + elif not should_device_be_active and is_device_active: + _LOGGER.info( + "%s - The valve should not be active (percent_open=%.0f), but the underlying valve is open (current_valve_opening=%.0f). Closing valve %s", + self, + self._percent_open or 9999, + self._last_sent_opening_value or 9999, + self._entity_id, + ) + await self.send_percent_open(fixed_value=self._min_open) + + async def send_percent_open(self, fixed_value: int = None): + """Send the percent open to the underlying valve""" + # This may fails if called after shutdown + value = self._percent_open if fixed_value is None else fixed_value + await self.send_value_to_number(self._entity_id, value) + self._last_sent_opening_value = value + + async def turn_off(self): + """Turn heater toggleable device off.""" + _LOGGER.debug("%s - Stopping underlying valve entity %s", self, self._entity_id) + # Issue 341 + is_active = self.is_device_active + self._percent_open = self.clamp_sent_value(0) + if is_active: + await self.send_percent_open() + + async def turn_on(self): + """Nothing to do for Valve because it cannot be turned on""" + await self.set_valve_open_percent() + + async def set_hvac_mode(self, hvac_mode: VThermHvacMode) -> bool: + """Set the HVACmode. Returns true if something have change""" + + if hvac_mode == VThermHvacMode_OFF and self.is_device_active: + await self.turn_off() + + if hvac_mode != VThermHvacMode_OFF and not self.is_device_active: + await self.turn_on() + + if self._hvac_mode != hvac_mode: + self._hvac_mode = hvac_mode + return True + else: + return False + + @property + def should_device_be_active(self) -> bool: + """If the toggleable device is currently active.""" + try: + return self._percent_open > (self._min_open or 0) if isinstance(self._percent_open, (int, float)) else False + except Exception: # pylint: disable=broad-exception-caught + return False + + @property + def is_device_active(self) -> bool | None: + """If the toggleable device is currently active.""" + if (current_opening := self.current_valve_opening) is None: + return None + + return current_opening > (self._min_open or 0) + + @overrides + def clamp_sent_value(self, value) -> float: + """Try to adapt the open_percent value to the min / max found + in the underlying entity (if any)""" + if not self.is_initialized: + raise RuntimeError(f"{self} - cannot clamp sent value because underlying is not initialized") + # return value + + # Gets the last number state + new_value = round(max(self._min_open, min(value / 100 * self._max_open, self._max_open))) + + if new_value != value: + _LOGGER.info( + "%s - Valve open percent have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f", + self, + new_value, + value, + self._min_open, + self._max_open, + ) + + return new_value + + async def set_valve_open_percent(self): + """Update the valve open percent""" + caped_val = self.clamp_sent_value(self._thermostat.valve_open_percent) + if self._percent_open == caped_val: + # No changes + return + + self._percent_open = caped_val + # Send the new command to valve via a service call + + _LOGGER.info( + "%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()) + await self.send_percent_open() + + def remove_entity(self): + """Remove the entity""" + super().remove_entity() + + @property + def percent_open(self) -> int: + """The current percent open""" + return self._percent_open + + @property + def last_sent_opening_value(self) -> int | None: + """Return the last sent value to the valve""" + return self._last_sent_opening_value + + @overrides + async def check_and_repair(self) -> bool: + """Check if the valve opening matches the last sent value and repair if needed. + Returns True if a repair was performed.""" + last_sent = self._last_sent_opening_value + current = self.current_valve_opening + + if last_sent is None or current is None: + return False + + _LOGGER.debug("%s - Checking if underlying valve state needs repair. last_sent=%s, current=%s", self, last_sent, current) + + if abs(current - last_sent) <= 0.5: + return False + + await self.send_percent_open() + return True + + @property + def current_valve_opening(self) -> float | None: + """Get the current valve opening from the underlying entity""" + + if (valve_state := self._state_manager.get_state(self.entity_id)) is None: + return None + valve_open: float = get_safe_float_value(valve_state.state) + return valve_open + +# ---------------------------------------------------------------- +# UnderlyingValveRegulation +# ---------------------------------------------------------------- +class UnderlyingValveRegulation(UnderlyingValve): + """A specific underlying class for Valve regulation""" + + def __init__( + self, + hass: HomeAssistant, + thermostat: Any, + opening_degree_entity_id: str, + closing_degree_entity_id: str, + climate_underlying: UnderlyingClimate, + min_opening_degree: int = 0, + max_opening_degree: int = 100, + max_closing_degree: int = 100, + opening_threshold: int = 0, + ) -> None: + """Initialize the underlying TRV with valve regulation""" + super().__init__( + hass, + thermostat, + opening_degree_entity_id, + entity_type=UnderlyingEntityType.VALVE_REGULATION, + ) + self._opening_degree_entity_id: str = opening_degree_entity_id + self._closing_degree_entity_id: str = closing_degree_entity_id + self._has_max_closing_degree: bool = closing_degree_entity_id is not None + self._climate_underlying = climate_underlying + self._max_opening_degree: float = max_opening_degree + self._min_opening_degree: int = min_opening_degree + self._max_closing_degree: int = max_closing_degree + self._opening_threshold: int = opening_threshold + + if self._min_opening_degree >= self._max_opening_degree: + self._min_opening_degree = self._opening_threshold + _LOGGER.error( + "min_opening_degree must be less than the max value of the opening degree of the entity {self._opening_degree_entity_id}. Value has been defaulted to opening threshold ({self._opening_threshold})" + ) + + @overrides + async def check_initial_state(self): + """Handle initial valve state change and hvac_mode""" + + _LOGGER.debug("%s - Starting initial state check for valve regulation underlying", self) + + # Initialize valve state and min max opening + self.init_valve_state_min_max_open() + + hvac_mode = self._thermostat.vtherm_hvac_mode + device_valve_opening = self.current_valve_opening # the real opening value + + # cuurent_hvac_mode_is_active = self._state_manager.get_state(self._climate_underlying.entity_id).state not in [HVACMode.OFF, STATE_UNAVAILABLE, STATE_UNKNOWN] + + should_be_on = hvac_mode != VThermHvacMode_OFF and not self._thermostat.is_sleeping + # is_on = cuurent_hvac_mode_is_active or (device_valve_opening is not None and device_valve_opening > self._opening_threshold) + is_on = device_valve_opening is not None and device_valve_opening > self._opening_threshold + + if should_be_on and not is_on: + _LOGGER.debug( + "%s - The valve should be active (hvac_mode=%s, sleeping=%s, percent_open=%.0f), but the underlying device is off or below threshold (current_valve_opening=%.0f). Opening valve %s", + self, + hvac_mode, + self._thermostat.is_sleeping, + self._percent_open or 9999, + device_valve_opening or 9999, + self._entity_id, + ) + # await self._climate_underlying.set_hvac_mode(hvac_mode) + if self._thermostat.is_sleeping: + self._percent_open = 100 + _LOGGER.debug("%s - is sleeping, setting percent_open to 100", self) + await self.send_percent_open() + else: + calculated_percent = self._thermostat.valve_open_percent + if calculated_percent is not None and calculated_percent > 0: + # TPI says heating is needed — respect the calculated value + self._percent_open = calculated_percent + _LOGGER.debug("%s - TPI says %.0f%% heating needed — opening valve", self, calculated_percent) + await self.send_percent_open() + else: + # TPI says 0% (no heating needed) — do not open the valve here. + # The normal startup flow will send the correct 0% command separately. + _LOGGER.debug( + "%s - Should be on (hvac_mode=%s) but TPI says 0%% or not yet calculated — leaving valve closed to avoid race condition", + self, + hvac_mode, + ) + + elif not should_be_on and is_on: + _LOGGER.debug( + "%s - The hvac mode is OFF and not sleeping, but the underlying device is not at off. Setting to %d%% device %s", + self, + self._opening_threshold, + self._entity_id, + ) + self._percent_open = self._opening_threshold + _LOGGER.debug("%s - off and not sleeping, setting percent_open to %d", self, self._percent_open) + await self.send_percent_open() + # await self._climate_underlying.set_hvac_mode(hvac_mode) + + def startup(self): + """Startup the Entity. Listen to the underlying state changes""" + + # Register the valve listener + # super().startup() + + # starts listening and can provide the initial cached state. + # TODO peut être que écouter self._opening_degree_entity_id ne sert à rien ici puisque c'est super() qui le fait + entities = [self._opening_degree_entity_id] + if self._has_max_closing_degree: + entities.append(self._closing_degree_entity_id) + self._state_manager.add_underlying_entities(entities) + + async def send_percent_open(self, fixed_value: int = None): + """Send the percent open to the underlying valve""" + # Caclulate percent_open + value = self._percent_open if fixed_value is None else fixed_value + opening_degree, closing_degree = OpeningClosingDegreeCalculation.calculate_opening_closing_degree( + brut_valve_open_percent=value, + min_opening_degree=self._min_opening_degree, + max_closing_degree=self._max_closing_degree, + max_opening_degree=self._max_opening_degree, + opening_threshold=self._opening_threshold, + ) + + # Send opening_degree in UnderlyingValve + await super().send_percent_open(opening_degree) + + if self.has_closing_degree_entity: + await self.send_value_to_number(self._closing_degree_entity_id, closing_degree) + + _LOGGER.debug( + "%s - valve regulation - I have sent opening_degree=%s closing_degree=%s", + self, + opening_degree, + closing_degree, + ) + + @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 min_opening_degree(self) -> int: + """The minimum opening degree""" + return self._min_opening_degree + + @property + def has_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 hvac_modes(self) -> list[VThermHvacMode]: + """Get the hvac_modes""" + if not self.is_initialized: + return [] + return [VThermHvacMode_HEAT, VThermHvacMode_SLEEP, VThermHvacMode_OFF] + + @property + def is_device_active(self): + """If the opening valve is open. The real opening value is used here. So we need to compare with 100 - max_closing which the max closing value if the valve is closed.""" + val = self.current_valve_opening + return val > (100 - self._max_closing_degree) if isinstance(val, (int, float)) else False + + @property + def should_device_be_active(self): + """If the opening valve is open. The real opening value is used here. So we need to compare with 100 - max_closing which the max closing value if the valve is closed.""" + # return value > (100 - self._max_closing_degree) # TODO why this ? + return self.hvac_mode not in [VThermHvacMode_OFF] and self._percent_open > self._opening_threshold if isinstance(self._percent_open, (int, float)) else False + + @property + def hvac_action(self) -> HVACAction: + """Calculate a hvac_action""" + if (value := self.last_sent_opening_value) is None: + return HVACAction.OFF + + # Align the behavior with is_device_active + if self.is_device_active: + return HVACAction.HEATING + elif value > 0: + return HVACAction.IDLE + else: + return HVACAction.OFF + + @property + def valve_entity_ids(self) -> List[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, + ]: + if entity: + ret.append(entity) + return ret + + @overrides + async def turn_off(self): + """Turn valve off. In that context it means set the valve to the minimum opening degree.""" + _LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id) + self._percent_open = 0 + await self.send_percent_open() + await self._climate_underlying.set_hvac_mode(VThermHvacMode_OFF) + +T = TypeVar("T", bound=UnderlyingEntity) diff --git a/custom_components/versatile_thermostat/vtherm_central_api.py b/custom_components/versatile_thermostat/vtherm_central_api.py new file mode 100644 index 0000000..2ba6c23 --- /dev/null +++ b/custom_components/versatile_thermostat/vtherm_central_api.py @@ -0,0 +1,241 @@ +""" The API of Versatile Thermostat""" + +from vtherm_api.log_collector import get_vtherm_logger +from vtherm_api.vtherm_api import VThermAPI +from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntryState + +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.number import NumberEntity + +from .const import ( + DOMAIN, + CONF_NAME, + CONF_AUTO_REGULATION_EXPERT, + CONF_SHORT_EMA_PARAMS, + CONF_SAFETY_MODE, + CONF_THERMOSTAT_TYPE, + CONF_THERMOSTAT_CENTRAL_CONFIG, + CONF_MAX_ON_PERCENT, +) + +from .feature_central_power_manager import FeatureCentralPowerManager +from .feature_central_boiler_manager import FeatureCentralBoilerManager + +_LOGGER = get_vtherm_logger(__name__) + + +class VersatileThermostatAPI(VThermAPI): + """The VersatileThermostatAPI""" + + def __init__(self) -> None: + _LOGGER.debug("building a VersatileThermostatAPI") + + super().__init__() + hass = self.hass + + self._expert_params = None + self._short_ema_params = None + self._safety_mode = None + self._central_configuration = None + 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 + self._central_power_manager = FeatureCentralPowerManager(hass, self) + self._central_boiler_manager = FeatureCentralBoilerManager(hass, self) + + # the current time (for testing purpose) + self._now = None + + # Flag to skip reload when a config entry update is triggered by the auto-TPI + # learning system (e.g. saving Kext). When True, update_listener will return + # immediately without reloading the config entry. + self.skip_reload_on_config_update: bool = False + + def find_central_configuration(self): + """Search for a central configuration""" + if not self._central_configuration: + for config_entry in self.hass.config_entries.async_entries(DOMAIN): + if ( + config_entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG + and config_entry.disabled_by is None + and config_entry.state + not in ( + ConfigEntryState.FAILED_UNLOAD, + ConfigEntryState.SETUP_ERROR, + ConfigEntryState.MIGRATION_ERROR, + ConfigEntryState.SETUP_RETRY, + ) + ): + self._central_configuration = config_entry + break + return self._central_configuration + + def reset_central_config(self): + """Reset the central configuration""" + self._central_configuration = None + + def set_global_config(self, config): + """Read the global configuration from configuration.yaml file""" + _LOGGER.info("Read global config from configuration.yaml") + + self._expert_params = config.get(CONF_AUTO_REGULATION_EXPERT) + if self._expert_params: + _LOGGER.debug("We have found expert params %s", self._expert_params) + + self._short_ema_params = config.get(CONF_SHORT_EMA_PARAMS) + if self._short_ema_params: + _LOGGER.debug("We have found short ema params %s", self._short_ema_params) + + self._safety_mode = config.get(CONF_SAFETY_MODE) + 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_temperature_number( + self, + config_id: str, + preset_name: str, + number_entity: NumberEntity, + ): + """Register the NumberEntity for a particular device / preset.""" + # Search for device_name into the _number_temperatures dict + if not self._number_temperatures.get(config_id): + self._number_temperatures[config_id] = dict() + + self._number_temperatures[config_id][preset_name] = number_entity + + def get_temperature_number_value(self, config_id, preset_name) -> float | None: + """Returns the value of a previously registred NumberEntity which represent + a temperature. If no NumberEntity was previously registred, then returns None""" + entities = self._number_temperatures.get(config_id, None) + if entities: + entity = entities.get(preset_name, None) + if entity: + return entity.state + return None + + 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.central_boiler_manager.reload_central_boiler_binary_listener() + await self.central_boiler_manager.reload_central_boiler_entities_list() + # Initialization of all preset for all VTherm + component: EntityComponent[ClimateEntity] = self._hass.data.get( + CLIMATE_DOMAIN, None + ) + if component: + for entity in list(component.entities): + # A little hack to test if the climate is a VTherm. Cannot use isinstance + # due to circular dependency of BaseThermostat + try: + if entity.device_info and entity.device_info.get("model", None) == DOMAIN: + if entry_id is None or entry_id == entity.unique_id: + await entity.async_startup(self.find_central_configuration()) # pyright: ignore[reportAttributeAccessIssue] + except Exception as e: # pylint: disable=broad-except + _LOGGER.error("Error searching/initializing entity %s: %s", entity.entity_id, e) + + # start listening for the central manager if not only one vtherm reload + if not entry_id: + await self.central_power_manager.start_listening() + await self.central_boiler_manager.start_listening() + + async def init_vtherm_preset_with_central(self): + """Init all VTherm presets when the VTherm uses central temperature""" + # Initialization of all preset for all VTherm + component: EntityComponent[ClimateEntity] = self._hass.data.get( + CLIMATE_DOMAIN, None + ) + if component: + for entity in list(component.entities): + if entity.device_info and entity.device_info.get("model", None) == DOMAIN and entity.use_central_config_temperature: # pyright: ignore[reportAttributeAccessIssue] + await entity.init_presets(self.find_central_configuration()) # pyright: ignore[reportAttributeAccessIssue] + entity.requested_state.force_changed() # pyright: ignore[reportAttributeAccessIssue] + await entity.update_states(True) # pyright: ignore[reportAttributeAccessIssue] + + def register_central_mode_select(self, central_mode_select): + """Register the select entity which holds the central_mode""" + self._central_mode_select = central_mode_select + + async def notify_central_mode_change(self, old_central_mode: str | None = None): + """Notify all VTherm that the central_mode have change""" + if self._central_mode_select is None: + return + + # Update all VTherm states + component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN] + for entity in list(component.entities): + if entity.device_info and entity.device_info.get("model", None) == DOMAIN: + _LOGGER.debug( + "Changing the central_mode. We have find %s to update", + entity.name, + ) + await entity.check_central_mode( + self._central_mode_select.state, old_central_mode + ) + + def add_entry(self, entry: ConfigEntry): + """Add a new entry""" + name = entry.data.get(CONF_NAME) + _LOGGER.debug("%s - Add the entry %s - %s", name, entry.entry_id, name) + # Add the entry in hass.data + self.hass.data[DOMAIN][entry.entry_id] = entry + + def remove_entry(self, entry: ConfigEntry): + """Remove an entry""" + name = entry.data.get(CONF_NAME) + _LOGGER.debug("%s - Remove the entry %s - %s", name, entry.entry_id, name) + self.hass.data[DOMAIN].pop(entry.entry_id) + # If not more entries are preset, remove the API + if len([val for val in self.hass.data[DOMAIN].values() if isinstance(val, ConfigEntry)]) == 0: + _LOGGER.debug("No more entries-> Remove the API from DOMAIN") + if DOMAIN in self.hass.data: + self.hass.data.pop(DOMAIN) + + @property + def self_regulation_expert(self): + """Get the self regulation params""" + return self._expert_params + + @property + def short_ema_params(self): + """Get the short EMA params in expert mode""" + return self._short_ema_params + + @property + def safety_mode(self): + """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_mode(self) -> str | None: + """Get the current central mode or None""" + if self._central_mode_select: + return self._central_mode_select.state + else: + return None + + @property + def central_power_manager(self) -> any: + """Returns the central power manager""" + return self._central_power_manager + + @property + def central_boiler_manager(self) -> any: + """Returns the central boiler manager""" + return self._central_boiler_manager diff --git a/custom_components/versatile_thermostat/vtherm_hvac_mode.py b/custom_components/versatile_thermostat/vtherm_hvac_mode.py new file mode 100644 index 0000000..e602ab7 --- /dev/null +++ b/custom_components/versatile_thermostat/vtherm_hvac_mode.py @@ -0,0 +1,164 @@ +""" A custom data class to manage specific HVAC modes for VTherm. """ + +import logging +from vtherm_api.log_collector import get_vtherm_logger +from homeassistant.components.climate.const import HVACMode +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN + +OFF = HVACMode.OFF.value +HEAT = HVACMode.HEAT.value +COOL = HVACMode.COOL.value +AUTO = HVACMode.AUTO.value +DRY = HVACMode.DRY.value +FAN_ONLY = HVACMode.FAN_ONLY.value +HEAT_COOL = HVACMode.HEAT_COOL.value +SLEEP = "sleep" + +VALID_HVAC_MODES = {OFF, HEAT, COOL, AUTO, DRY, FAN_ONLY, HEAT_COOL, SLEEP, STATE_UNAVAILABLE, STATE_UNKNOWN} + +_LOGGER = get_vtherm_logger(__name__) + +class VThermHvacMode: + """ + Container and validator for supported HVAC operating modes. + """ + def __init__(self, hvac_mode: str): + """ + Initialize a VThermHvacMode instance. + + Args: + hvac_mode (VThermHvacMode): The HVAC mode to set. + + Raises: + ValueError: If the mode is not supported. + """ + if str(hvac_mode) not in VALID_HVAC_MODES or not isinstance(hvac_mode, str): + _LOGGER.warning("Unsupported HVAC mode: '%s'. It may be temporary at startup. Your VTherm will be set to OFF mode.", hvac_mode) + hvac_mode = STATE_UNKNOWN + self._hvac_mode: str = hvac_mode + + def __str__(self): + return self._hvac_mode + + def __eq__(self, other: object) -> bool: + if isinstance(other, str): + return self._hvac_mode == other + else: + return self._hvac_mode == str(other) + + def __repr__(self): + return f"[VThermHvacMode: {self._hvac_mode}]" + + def to_json(self) -> str: + """Convert to JSON serializable string.""" + return self._hvac_mode + + @classmethod + def from_json(cls, json_value: str) -> "VThermHvacMode": + """Create instance from JSON serializable string.""" + return cls(json_value) + + +VThermHvacMode_OFF = VThermHvacMode(OFF) +VThermHvacMode_HEAT = VThermHvacMode(HEAT) +VThermHvacMode_COOL = VThermHvacMode(COOL) +VThermHvacMode_AUTO = VThermHvacMode(AUTO) +VThermHvacMode_DRY = VThermHvacMode(DRY) +VThermHvacMode_FAN_ONLY = VThermHvacMode(FAN_ONLY) +VThermHvacMode_HEAT_COOL = VThermHvacMode(HEAT_COOL) +VThermHvacMode_SLEEP = VThermHvacMode(SLEEP) +VThermHvacMode_UNKNOWN = VThermHvacMode(STATE_UNKNOWN) +VThermHvacMode_UNAVAILABLE = VThermHvacMode(STATE_UNAVAILABLE) + +# Map statique pour conversion HA -> VTherm +HA_TO_VTHERM_MAP = { + HVACMode.OFF: VThermHvacMode_OFF, + HVACMode.HEAT: VThermHvacMode_HEAT, + HVACMode.COOL: VThermHvacMode_COOL, + HVACMode.AUTO: VThermHvacMode_AUTO, + HVACMode.DRY: VThermHvacMode_DRY, + HVACMode.FAN_ONLY: VThermHvacMode_FAN_ONLY, + HVACMode.HEAT_COOL: VThermHvacMode_HEAT_COOL, + SLEEP: VThermHvacMode_SLEEP, + STATE_UNAVAILABLE: VThermHvacMode_UNAVAILABLE, + STATE_UNKNOWN: VThermHvacMode_UNKNOWN, +} + +# Map statique pour conversion VTherm -> HA +VTHERM_TO_HA_MAP = { + OFF: HVACMode.OFF, + HEAT: HVACMode.HEAT, + COOL: HVACMode.COOL, + AUTO: HVACMode.AUTO, + DRY: HVACMode.DRY, + FAN_ONLY: HVACMode.FAN_ONLY, + HEAT_COOL: HVACMode.HEAT_COOL, + SLEEP: SLEEP, # SLEEP doesn't exist in HA, we map to a new string + STATE_UNAVAILABLE: STATE_UNAVAILABLE, + STATE_UNKNOWN: STATE_UNKNOWN, +} + +VTHERM_TO_LEGACY_HA_MAP = { + OFF: HVACMode.OFF, + HEAT: HVACMode.HEAT, + COOL: HVACMode.COOL, + AUTO: HVACMode.AUTO, + DRY: HVACMode.DRY, + FAN_ONLY: HVACMode.FAN_ONLY, + HEAT_COOL: HVACMode.HEAT_COOL, + SLEEP: HVACMode.OFF, # SLEEP is mapped to OFF + STATE_UNAVAILABLE: STATE_UNAVAILABLE, + STATE_UNKNOWN: STATE_UNKNOWN, +} + + +def from_ha_hvac_mode(ha_hvac_mode: HVACMode | None) -> VThermHvacMode | None: + """ + Convert a Home Assistant HVACMode (enum or str) to a VThermHvacMode instance. + + Args: + ha_hvac_mode (HVACMode | str): The Home Assistant HVAC mode. + + Returns: + VThermHvacMode: The corresponding VThermHvacMode instance. + """ + if ha_hvac_mode is None: + return None + + # Si enum, utiliser la map + if isinstance(ha_hvac_mode, HVACMode): + converter = HA_TO_VTHERM_MAP.get(ha_hvac_mode) + if converter: + return converter + else: + raise ValueError(f"HA HVACMode not found: {ha_hvac_mode}") + + +def to_ha_hvac_mode(vtherm_hvac_mode: VThermHvacMode | None) -> HVACMode | None: + """ + Convert a VThermHvacMode instance to a Home Assistant HVACMode (enum or str). + + Args: + vtherm_hvac_mode (VThermHvacMode): The VThermHvacMode instance. + + Returns: + HVACMode | None: The corresponding Home Assistant HVACMode or None. + """ + if vtherm_hvac_mode is None: + return None + return VTHERM_TO_HA_MAP.get(str(vtherm_hvac_mode)) + + +def to_legacy_ha_hvac_mode(vtherm_hvac_mode: VThermHvacMode | None) -> HVACMode | None: + """ + Convert a VThermHvacMode instance to a Home Assistant HVACMode (enum or str) and replace SLEEP with OFF. + + Args: + vtherm_hvac_mode (VThermHvacMode): The VThermHvacMode instance. + + Returns: + HVACMode | None: The corresponding Home Assistant HVACMode or None. + """ + if vtherm_hvac_mode is None: + return None + return VTHERM_TO_LEGACY_HA_MAP.get(str(vtherm_hvac_mode)) diff --git a/custom_components/versatile_thermostat/vtherm_preset.py b/custom_components/versatile_thermostat/vtherm_preset.py new file mode 100644 index 0000000..cd0d6c3 --- /dev/null +++ b/custom_components/versatile_thermostat/vtherm_preset.py @@ -0,0 +1,58 @@ +""" A custom data class to manage specific presets for VTherm. """ + +from homeassistant.components.climate.const import PRESET_NONE, PRESET_BOOST, PRESET_ECO, PRESET_COMFORT, PRESET_ACTIVITY + +# from .const import PRESET_SAFETY, PRESET_POWER, PRESET_FROST_PROTECTION + +# Liste des presets valides (à adapter selon ton projet) +PRESET_POWER = "power" +PRESET_SAFETY = "safety" +PRESET_FROST_PROTECTION = "frost" + +PRESET_TEMP_SUFFIX = "_temp" +PRESET_AC_SUFFIX = "_ac" +PRESET_AWAY_SUFFIX = "_away" + + +# Constants for all VTherm presets +class VThermPreset(str): + """The List of Preset used by VTherm""" + + NONE = PRESET_NONE + FROST = PRESET_FROST_PROTECTION + ECO = PRESET_ECO + COMFORT = PRESET_COMFORT + BOOST = PRESET_BOOST + SAFETY = PRESET_SAFETY + POWER = PRESET_POWER + ACTIVITY = PRESET_ACTIVITY + + +class VThermPresetWithAC(str): + """Supplementary preset for AC hvac mode""" + + ECO = VThermPreset.ECO + PRESET_AC_SUFFIX + COMFORT = VThermPreset.COMFORT + PRESET_AC_SUFFIX + BOOST = VThermPreset.BOOST + PRESET_AC_SUFFIX + FROST = VThermPreset.FROST + PRESET_AC_SUFFIX + + +class VThermPresetWithAway(str): + """The List of Preset of Away type used by VTherm""" + + FROST = VThermPreset.FROST + PRESET_AWAY_SUFFIX + ECO = VThermPreset.ECO + PRESET_AWAY_SUFFIX + COMFORT = VThermPreset.COMFORT + PRESET_AWAY_SUFFIX + BOOST = VThermPreset.BOOST + PRESET_AWAY_SUFFIX + + +class VThermPresetWithACAway(str): + """The List of Preset for Ac and Away used by VTherm""" + + FROST = VThermPreset.FROST + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX + ECO = VThermPreset.ECO + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX + COMFORT = VThermPreset.COMFORT + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX + BOOST = VThermPreset.BOOST + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX + + +HIDDEN_PRESETS = [VThermPreset.POWER, VThermPreset.SAFETY] diff --git a/custom_components/versatile_thermostat/vtherm_state.py b/custom_components/versatile_thermostat/vtherm_state.py new file mode 100644 index 0000000..f7dd9b2 --- /dev/null +++ b/custom_components/versatile_thermostat/vtherm_state.py @@ -0,0 +1,178 @@ +"""State container for Versatile Thermostat. + +This lightweight data class is intended to group together the minimal +public state we may want to persist, serialize, expose via services +or exchange between managers (for example during algorithms decisions +or for debug tooling). + +Attributes: + hvac_mode: Current HVAC mode of the thermostat (e.g. 'heat', 'off', 'cool', 'sleep'). + target_temperature: The active target temperature (can be None when mode does not use a temperature, e.g. 'off'). + preset: The current preset name (e.g. 'eco', 'comfort', 'boost', 'away') or None if not in a preset. +""" +from __future__ import annotations + +from typing import Optional, Any + +from .vtherm_hvac_mode import VThermHvacMode +from .vtherm_preset import VThermPreset + + +class VThermState: + """Simple state snapshot for a VTherm. + + The object is mutable; use set_state for a bulk update or assign + attributes individually via properties. to_dict / from_dict help (de)serialization. + """ + + def __init__(self, hvac_mode: Any, target_temperature: Optional[float] = None, preset: Optional[VThermPreset] = None) -> None: + if preset is not None and not isinstance(preset, str): + raise ValueError(f"Invalid preset: {preset}. Should be an instance of VThermPreset.") + + self._hvac_mode: VThermHvacMode = hvac_mode if isinstance(hvac_mode, VThermHvacMode) else VThermHvacMode(str(hvac_mode)) + self._target_temperature: Optional[float] = target_temperature + self._preset: Optional[VThermPreset] = preset + self._is_hvac_mode_changed: bool = True + self._is_target_temperature_changed: bool = True + self._is_preset_changed: bool = True + + def set_state( + self, + hvac_mode: Optional[VThermHvacMode] = None, + target_temperature: Optional[float] = None, + preset: Optional[VThermPreset] = None, + ) -> None: + """Update only the attributes provided (not None). + + Args: + hvac_mode: New HVAC mode to set (if not None). + target_temperature: New target temperature (if not None). + preset: New preset (if not None). + """ + if hvac_mode is not None: + self.set_hvac_mode(hvac_mode) + if target_temperature is not None: + self.set_target_temperature(target_temperature) + if preset is not None: + self.set_preset(preset) + + def set_hvac_mode(self, hvac_mode: VThermHvacMode) -> None: + """Set the HVAC mode only. + + Args: + hvac_mode: New HVAC mode to set (e.g. 'heat', 'off', 'cool', 'sleep'). + """ + if not isinstance(hvac_mode, VThermHvacMode): + hvac_mode = VThermHvacMode(str(hvac_mode)) + + self._is_hvac_mode_changed = self._is_hvac_mode_changed or self._hvac_mode != hvac_mode + self._hvac_mode = hvac_mode + + def set_target_temperature(self, target_temperature: Optional[float]) -> None: + """Set the target temperature only. + + Args: + target_temperature: New target temperature to set, or None. + """ + self._is_target_temperature_changed = self._is_target_temperature_changed or self._target_temperature != target_temperature + self._target_temperature = target_temperature + + def set_preset(self, preset: Optional[VThermPreset]) -> None: + """Set the preset only. + + Args: + preset: New preset name to set, or None. + """ + if preset is not None and not isinstance(preset, str): + raise ValueError(f"Invalid preset: {preset}. Should be an instance of VThermPreset.") + + self._is_preset_changed = self._is_preset_changed or self._preset != preset + self._preset = preset + + def force_changed(self) -> None: + """Forcefully mark the state as changed.""" + self._is_hvac_mode_changed = True + self._is_target_temperature_changed = True + self._is_preset_changed = True + + @property + def hvac_mode(self) -> VThermHvacMode: + """Get or set the current HVAC mode.""" + return self._hvac_mode + + @property + def target_temperature(self) -> Optional[float]: + """Get or set the current target temperature.""" + return self._target_temperature + + @property + def preset(self) -> Optional[VThermPreset]: + """Get or set the current preset.""" + return self._preset + + @property + def is_changed(self) -> bool: + """Check if the state has changed.""" + return self._is_hvac_mode_changed or self._is_target_temperature_changed or self._is_preset_changed + + @property + def is_hvac_mode_changed(self) -> bool: + """Check if the HVAC mode has changed.""" + return self._is_hvac_mode_changed + + @property + def is_target_temperature_changed(self) -> bool: + """Check if the target temperature has changed.""" + return self._is_target_temperature_changed + + @property + def is_preset_changed(self) -> bool: + """Check if the preset has changed.""" + return self._is_preset_changed + + def reset_changed(self) -> None: + """Reset the changed state.""" + self._is_hvac_mode_changed = False + self._is_target_temperature_changed = False + self._is_preset_changed = False + + def to_dict(self) -> dict[str, Any]: + """Convert the state to a dictionary.""" + return { + "hvac_mode": str(self._hvac_mode), + "target_temperature": self._target_temperature, + "preset": str(self._preset), + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "VThermState": + """Create a VThermState from a dictionary.""" + hvac_mode = VThermHvacMode(data["hvac_mode"]) + target_temperature = data["target_temperature"] + preset = VThermPreset(data["preset"]) if data["preset"] else None + state = cls(hvac_mode, target_temperature, preset) + return state + + def __str__(self) -> str: + """Return a human readable representation of the state.""" + return f"VThermState(" f"hvac_mode={self._hvac_mode}, " f"target_temperature={self._target_temperature}, " f"preset={self._preset}, " f"is_changed={self.is_changed})" + + __repr__ = __str__ + + def __eq__(self, other: object) -> bool: + """Compare two VThermState instances for equality. + + Args: + other: Another object to compare with. + + Returns: + True if both states have the same hvac_mode, target_temperature, and preset. + """ + if not isinstance(other, VThermState): + return False + + return self._hvac_mode == other._hvac_mode and self._target_temperature == other._target_temperature and self._preset == other._preset + + def __ne__(self, other: object) -> bool: + """Compare two VThermState instances for inequality.""" + return not self.__eq__(other) diff --git a/www/community/versatile-thermostat-ui-card/versatile-thermostat-ui-card.js b/www/community/versatile-thermostat-ui-card/versatile-thermostat-ui-card.js new file mode 100644 index 0000000..7eee151 --- /dev/null +++ b/www/community/versatile-thermostat-ui-card/versatile-thermostat-ui-card.js @@ -0,0 +1,2912 @@ +function t(t,e,i,o){var n,a=arguments.length,s=a<3?e:null===o?o=Object.getOwnPropertyDescriptor(e,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,i,o);else for(var r=t.length-1;r>=0;r--)(n=t[r])&&(s=(a<3?n(s):a>3?n(e,i,s):n(e,i))||s);return a>3&&s&&Object.defineProperty(e,i,s),s}"function"==typeof SuppressedError&&SuppressedError; +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const e=globalThis,i=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,o=Symbol(),n=new WeakMap;let a=class{constructor(t,e,i){if(this._$cssResult$=!0,i!==o)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const e=this.t;if(i&&void 0===t){const i=void 0!==e&&1===e.length;i&&(t=n.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),i&&n.set(e,t))}return t}toString(){return this.cssText}};const s=(t,...e)=>{const i=1===t.length?t[0]:e.reduce(((e,i,o)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[o+1]),t[0]);return new a(i,t,o)},r=i?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new a("string"==typeof t?t:t+"",void 0,o))(e)})(t):t +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */,{is:l,defineProperty:c,getOwnPropertyDescriptor:d,getOwnPropertyNames:h,getOwnPropertySymbols:u,getPrototypeOf:m}=Object,p=globalThis,_=p.trustedTypes,g=_?_.emptyScript:"",f=p.reactiveElementPolyfillSupport,b=(t,e)=>t,v={toAttribute(t,e){switch(e){case Boolean:t=t?g:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},y=(t,e)=>!l(t,e),w={attribute:!0,type:String,converter:v,reflect:!1,hasChanged:y};Symbol.metadata??=Symbol("metadata"),p.litPropertyMetadata??=new WeakMap;let x=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=w){if(e.state&&(e.attribute=!1),this._$Ei(),this.elementProperties.set(t,e),!e.noAccessor){const i=Symbol(),o=this.getPropertyDescriptor(t,i,e);void 0!==o&&c(this.prototype,t,o)}}static getPropertyDescriptor(t,e,i){const{get:o,set:n}=d(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get(){return o?.call(this)},set(e){const a=o?.call(this);n.call(this,e),this.requestUpdate(t,a,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??w}static _$Ei(){if(this.hasOwnProperty(b("elementProperties")))return;const t=m(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(b("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(b("properties"))){const t=this.properties,e=[...h(t),...u(t)];for(const i of e)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,e]of this.elementProperties){const i=this._$Eu(t,e);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(r(t))}else void 0!==t&&e.push(r(t));return e}static _$Eu(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$Eg=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach((t=>t(this)))}addController(t){(this._$ES??=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$ES?.splice(this._$ES.indexOf(t)>>>0,1)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const i of e.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((t,o)=>{if(i)t.adoptedStyleSheets=o.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet));else for(const i of o){const o=document.createElement("style"),n=e.litNonce;void 0!==n&&o.setAttribute("nonce",n),o.textContent=i.cssText,t.appendChild(o)}})(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$ES?.forEach((t=>t.hostConnected?.()))}enableUpdating(t){}disconnectedCallback(){this._$ES?.forEach((t=>t.hostDisconnected?.()))}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$EO(t,e){const i=this.constructor.elementProperties.get(t),o=this.constructor._$Eu(t,i);if(void 0!==o&&!0===i.reflect){const n=(void 0!==i.converter?.toAttribute?i.converter:v).toAttribute(e,i.type);this._$Em=t,null==n?this.removeAttribute(o):this.setAttribute(o,n),this._$Em=null}}_$AK(t,e){const i=this.constructor,o=i._$Eh.get(t);if(void 0!==o&&this._$Em!==o){const t=i.getPropertyOptions(o),n="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:v;this._$Em=o,this[o]=n.fromAttribute(e,t.type),this._$Em=null}}requestUpdate(t,e,i,o=!1,n){if(void 0!==t){if(i??=this.constructor.getPropertyOptions(t),!(i.hasChanged??y)(o?n:this[t],e))return;this.C(t,e,i)}!1===this.isUpdatePending&&(this._$Eg=this._$EP())}C(t,e,i){this._$AL.has(t)||this._$AL.set(t,e),!0===i.reflect&&this._$Em!==t&&(this._$Ej??=new Set).add(t)}async _$EP(){this.isUpdatePending=!0;try{await this._$Eg}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,i]of t)!0!==i.wrapped||this._$AL.has(e)||void 0===this[e]||this.C(e,this[e],i)}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$ES?.forEach((t=>t.hostUpdate?.())),this.update(e)):this._$ET()}catch(e){throw t=!1,this._$ET(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$ES?.forEach((t=>t.hostUpdated?.())),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$ET(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$Eg}shouldUpdate(t){return!0}update(t){this._$Ej&&=this._$Ej.forEach((t=>this._$EO(t,this[t]))),this._$ET()}updated(t){}firstUpdated(t){}};x.elementStyles=[],x.shadowRootOptions={mode:"open"},x[b("elementProperties")]=new Map,x[b("finalized")]=new Map,f?.({ReactiveElement:x}),(p.reactiveElementVersions??=[]).push("2.0.1"); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const k=globalThis,T=k.trustedTypes,S=T?T.createPolicy("lit-html",{createHTML:t=>t}):void 0,M="$lit$",D=`lit$${(Math.random()+"").slice(9)}$`,C="?"+D,A=`<${C}>`,P=document,E=()=>P.createComment(""),L=t=>null===t||"object"!=typeof t&&"function"!=typeof t,$=Array.isArray,z="[ \t\n\f\r]",O=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,V=/-->/g,H=/>/g,R=RegExp(`>|${z}(?:([^\\s"'>=/]+)(${z}*=${z}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),I=/'/g,N=/"/g,F=/^(?:script|style|textarea|title)$/i,W=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),B=W(1),j=W(2),U=Symbol.for("lit-noChange"),Y=Symbol.for("lit-nothing"),q=new WeakMap,Z=P.createTreeWalker(P,129);function X(t,e){if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==S?S.createHTML(e):e}const K=(t,e)=>{const i=t.length-1,o=[];let n,a=2===e?"":"",s=O;for(let e=0;e"===l[0]?(s=n??O,c=-1):void 0===l[1]?c=-2:(c=s.lastIndex-l[2].length,r=l[1],s=void 0===l[3]?R:'"'===l[3]?N:I):s===N||s===I?s=R:s===V||s===H?s=O:(s=R,n=void 0);const h=s===R&&t[e+1].startsWith("/>")?" ":"";a+=s===O?i+A:c>=0?(o.push(r),i.slice(0,c)+M+i.slice(c)+D+h):i+D+(-2===c?e:h)}return[X(t,a+(t[i]||"")+(2===e?"":"")),o]};class G{constructor({strings:t,_$litType$:e},i){let o;this.parts=[];let n=0,a=0;const s=t.length-1,r=this.parts,[l,c]=K(t,e);if(this.el=G.createElement(l,i),Z.currentNode=this.el.content,2===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(o=Z.nextNode())&&r.length0){o.textContent=T?T.emptyScript:"";for(let i=0;i$(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.T(t):this._(t)}k(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}$(t){this._$AH!==t&&(this._$AR(),this._$AH=this.k(t))}_(t){this._$AH!==Y&&L(this._$AH)?this._$AA.nextSibling.data=t:this.$(P.createTextNode(t)),this._$AH=t}g(t){const{values:e,_$litType$:i}=t,o="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=G.createElement(X(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===o)this._$AH.p(e);else{const t=new J(o,this),i=t.u(this.options);t.p(e),this.$(i),this._$AH=t}}_$AC(t){let e=q.get(t.strings);return void 0===e&&q.set(t.strings,e=new G(t)),e}T(t){$(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,o=0;for(const n of t)o===e.length?e.push(i=new tt(this.k(E()),this.k(E()),this,this.options)):i=e[o],i._$AI(n),o++;o2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=Y}_$AI(t,e=this,i,o){const n=this.strings;let a=!1;if(void 0===n)t=Q(this,t,e,0),a=!L(t)||t!==this._$AH&&t!==U,a&&(this._$AH=t);else{const o=t;let s,r;for(t=n[0],s=0;s{const o=i?.renderBefore??e;let n=o._$litPart$;if(void 0===n){const t=i?.renderBefore??null;o._$litPart$=n=new tt(e.insertBefore(E(),t),t,void 0,i??{})}return n._$AI(t),n})(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return U}}rt._$litElement$=!0,rt.finalized=!0,globalThis.litElementHydrateSupport?.({LitElement:rt});const lt=globalThis.litElementPolyfillSupport;lt?.({LitElement:rt}),(globalThis.litElementVersions??=[]).push("4.0.1"); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const ct=t=>(e,i)=>{void 0!==i?i.addInitializer((()=>{customElements.define(t,e)})):customElements.define(t,e)} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */,dt={attribute:!0,type:String,converter:v,reflect:!1,hasChanged:y},ht=(t=dt,e,i)=>{const{kind:o,metadata:n}=i;let a=globalThis.litPropertyMetadata.get(n);if(void 0===a&&globalThis.litPropertyMetadata.set(n,a=new Map),a.set(i.name,t),"accessor"===o){const{name:o}=i;return{set(i){const n=e.get.call(this);e.set.call(this,i),this.requestUpdate(o,n,t)},init(e){return void 0!==e&&this.C(o,void 0,t),e}}}if("setter"===o){const{name:o}=i;return function(i){const n=this[o];e.call(this,i),this.requestUpdate(o,n,t)}}throw Error("Unsupported decorator location: "+o)};function ut(t){return(e,i)=>"object"==typeof i?ht(t,e,i):((t,e,i)=>{const o=e.hasOwnProperty(i);return e.constructor.createProperty(i,o?{...t,wrapped:!0}:t),o?Object.getOwnPropertyDescriptor(e,i):void 0})(t,e,i) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */}function mt(t){return ut({...t,state:!0,attribute:!1})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const pt=(t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect.decorate&&"object"!=typeof e&&Object.defineProperty(t,e,i),i) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;function _t(t,e){return(i,o,n)=>{const a=e=>e.renderRoot?.querySelector(t)??null;if(e){const{get:t,set:s}="object"==typeof o?i:n??(()=>{const t=Symbol();return{get(){return this[t]},set(e){this[t]=e}}})();return pt(i,o,{get(){if(e){let e=t.call(this);return void 0===e&&(e=a(this),s.call(this,e)),e}return a(this)}})}return pt(i,o,{get(){return a(this)}})}} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const gt=1;class ft{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}} +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const bt=(t=>(...e)=>({_$litDirective$:t,values:e}))(class extends ft{constructor(t){if(super(t),t.type!==gt||"class"!==t.name||t.strings?.length>2)throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.")}render(t){return" "+Object.keys(t).filter((e=>t[e])).join(" ")+" "}update(t,[e]){if(void 0===this.it){this.it=new Set,void 0!==t.strings&&(this.st=new Set(t.strings.join(" ").split(/\s/).filter((t=>""!==t))));for(const t in e)e[t]&&!this.st?.has(t)&&this.it.add(t);return this.render(e)}const i=t.element.classList;for(const t of this.it)t in e||(i.remove(t),this.it.delete(t));for(const t in e){const o=!!e[t];o===this.it.has(t)||this.st?.has(t)||(o?(i.add(t),this.it.add(t)):(i.remove(t),this.it.delete(t)))}return U}});var vt="M6.59,0.66C8.93,-1.15 11.47,1.06 12.04,4.5C12.47,4.5 12.89,4.62 13.27,4.84C13.79,4.24 14.25,3.42 14.07,2.5C13.65,0.35 16.06,-1.39 18.35,1.58C20.16,3.92 17.95,6.46 14.5,7.03C14.5,7.46 14.39,7.89 14.16,8.27C14.76,8.78 15.58,9.24 16.5,9.06C18.63,8.64 20.38,11.04 17.41,13.34C15.07,15.15 12.53,12.94 11.96,9.5C11.53,9.5 11.11,9.37 10.74,9.15C10.22,9.75 9.75,10.58 9.93,11.5C10.35,13.64 7.94,15.39 5.65,12.42C3.83,10.07 6.05,7.53 9.5,6.97C9.5,6.54 9.63,6.12 9.85,5.74C9.25,5.23 8.43,4.76 7.5,4.94C5.37,5.36 3.62,2.96 6.59,0.66M5,16H7A2,2 0 0,1 9,18V24H7V22H5V24H3V18A2,2 0 0,1 5,16M5,18V20H7V18H5M12.93,16H15L12.07,24H10L12.93,16M18,16H21V18H18V22H21V24H18A2,2 0 0,1 16,22V18A2,2 0 0,1 18,16Z",yt="M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M11,15H13V17H11V15M11,7H13V13H11V7",wt="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z",xt="M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z",kt="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z",Tt="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z",St="M12,11A1,1 0 0,0 11,12A1,1 0 0,0 12,13A1,1 0 0,0 13,12A1,1 0 0,0 12,11M12.5,2C17,2 17.11,5.57 14.75,6.75C13.76,7.24 13.32,8.29 13.13,9.22C13.61,9.42 14.03,9.73 14.35,10.13C18.05,8.13 22.03,8.92 22.03,12.5C22.03,17 18.46,17.1 17.28,14.73C16.78,13.74 15.72,13.3 14.79,13.11C14.59,13.59 14.28,14 13.88,14.34C15.87,18.03 15.08,22 11.5,22C7,22 6.91,18.42 9.27,17.24C10.25,16.75 10.69,15.71 10.89,14.79C10.4,14.59 9.97,14.27 9.65,13.87C5.96,15.85 2,15.07 2,11.5C2,7 5.56,6.89 6.74,9.26C7.24,10.25 8.29,10.68 9.22,10.87C9.41,10.39 9.73,9.97 10.14,9.65C8.15,5.96 8.94,2 12.5,2Z",Mt="M5,2V13H8V22L15,10H11L15,2M17,15H19V17H17V15M17,7H19V13H17V7Z",Dt="M8.5 4.5L5.4 9.5L8.5 14.7L5.2 20.5L3.4 19.6L6.1 14.7L3 9.5L6.7 3.6L8.5 4.5M14.7 4.4L11.6 9.5L14.7 14.5L11.4 20.3L9.6 19.4L12.3 14.5L9.2 9.5L12.9 3.5L14.7 4.4M21 4.4L17.9 9.5L21 14.5L17.7 20.3L15.9 19.4L18.6 14.5L15.5 9.5L19.2 3.5L21 4.4",Ct="M12,3L2,12H5V20H19V12H22L12,3M12,8.75A2.25,2.25 0 0,1 14.25,11A2.25,2.25 0 0,1 12,13.25A2.25,2.25 0 0,1 9.75,11A2.25,2.25 0 0,1 12,8.75M12,15C13.5,15 16.5,15.75 16.5,17.25V18H7.5V17.25C7.5,15.75 10.5,15 12,15Z",At="M13 9H11V7H13V9M13 17H11V11H13V17M5 3H19C20.1 3 21 3.89 21 5V19C21 19.53 20.79 20.04 20.41 20.41C20.04 20.79 19.53 21 19 21H5C4.47 21 3.96 20.79 3.59 20.41C3.21 20.04 3 19.53 3 19V5C3 3.89 3.89 3 5 3M19 19V5H5V19H19Z",Pt="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z",Et="M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10A2,2 0 0,1 6,8H15V6A3,3 0 0,0 12,3A3,3 0 0,0 9,6H7A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,17A2,2 0 0,0 14,15A2,2 0 0,0 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17Z",Lt="M12 2C7.04 2 3 6.04 3 11C3 14.91 5.5 18.24 9 19.47V22H11V19.94C11.33 20 11.66 20 12 20S12.67 20 13 19.94V22H15V19.47C18.5 18.23 21 14.9 21 11C21 6.04 16.96 2 12 2M14.25 14L11.25 17L9.75 15.5L11 14.25L9.75 13L12.75 10L14.25 11.5L13 12.75L14.25 14M16 9H8V7H16V9Z",$t="M10,0.2C9,0.2 8.2,1 8.2,2C8.2,3 9,3.8 10,3.8C11,3.8 11.8,3 11.8,2C11.8,1 11,0.2 10,0.2M15.67,1A7.33,7.33 0 0,0 23,8.33V7A6,6 0 0,1 17,1H15.67M18.33,1C18.33,3.58 20.42,5.67 23,5.67V4.33C21.16,4.33 19.67,2.84 19.67,1H18.33M21,1A2,2 0 0,0 23,3V1H21M7.92,4.03C7.75,4.03 7.58,4.06 7.42,4.11L2,5.8V11H3.8V7.33L5.91,6.67L2,22H3.8L6.67,13.89L9,17V22H10.8V15.59L8.31,11.05L9.04,8.18L10.12,10H15V8.2H11.38L9.38,4.87C9.08,4.37 8.54,4.03 7.92,4.03Z",zt="M18.73,18C15.4,21.69 9.71,22 6,18.64C2.33,15.31 2.04,9.62 5.37,5.93C6.9,4.25 9,3.2 11.27,3C7.96,6.7 8.27,12.39 12,15.71C13.63,17.19 15.78,18 18,18C18.25,18 18.5,18 18.73,18Z",Ot="M17 13V7H19V13H17M17 17V15H19V17H17M13 13V5C13 3.3 11.7 2 10 2S7 3.3 7 5V13C4.8 14.7 4.3 17.8 6 20S10.8 22.7 13 21 15.7 16.2 14 14C13.7 13.6 13.4 13.3 13 13M10 4C10.6 4 11 4.4 11 5V8H9V5C9 4.4 9.4 4 10 4Z",Vt="M12,3.25C12,3.25 6,10 6,14C6,17.32 8.69,20 12,20A6,6 0 0,0 18,14C18,10 12,3.25 12,3.25M14.47,9.97L15.53,11.03L9.53,17.03L8.47,15.97M9.75,10A1.25,1.25 0 0,1 11,11.25A1.25,1.25 0 0,1 9.75,12.5A1.25,1.25 0 0,1 8.5,11.25A1.25,1.25 0 0,1 9.75,10M14.25,14.5A1.25,1.25 0 0,1 15.5,15.75A1.25,1.25 0 0,1 14.25,17A1.25,1.25 0 0,1 13,15.75A1.25,1.25 0 0,1 14.25,14.5Z",Ht="M21 20V2H3V20H1V23H23V20M19 4V11H17V4M5 4H7V11H5M5 20V13H7V20M9 20V4H15V20M17 20V13H19V20Z",Rt="M2 4H20V8H18V20H16V8H6V20H4V8H2V4M7 9H15V11H7V9M7 12H15V14H7V12M20 19V17H22V19H20M20 15V10H22V15H20Z";var It={version:"version",current:"current"},Nt={card:{climate:{autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",allow_lock_toggle:"Allow lock/unlock from card",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Allow preset temperature modification",show_status_text:"Show current HVAC action text",show_background_gradient_on_active:"Show the slider background gradient when the entity is active",show_regulation_chart:"Show regulation chart"}}},Ft={window_open:"Window open",window_bypass:"Bypass window activated",night_mode:"Night mode",eco:"Eco",overpowering:"Overpowering detected",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active",lock_users_active:"User controls locked",lock_automations_active:"Automation changes locked","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",code_error:"Incorrect code",lock:"Lock",unlock:"Unlock",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Preset temperatures",preset_mod_no_entities:"No preset temperature entities found for this device.",preset_row_heat:"Heat",preset_row_heat_away:"Heat away",preset_row_cool:"Cool",preset_row_cool_away:"Cool away",regulation_chart_title:"Regulation chart"},Wt={common:It,editor:Nt,extra_states:Ft},Bt=Object.freeze({__proto__:null,common:It,default:Wt,editor:Nt,extra_states:Ft}),jt={version:"Version",current:"Aktuell"},Ut={card:{climate:{autoStartStopEnableEntity:"Auto-Start/Stop-Entität",powerEntity:"Entität für den Stromverbrauch",theme:"Thema",theme_classic:"Klassisch",theme_vtherm:"VTherm",theme_uncolored:"Ohne Farbe",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",disable_window:"Fenster deaktivieren",disable_autoStartStop:"Auto-Start/Stop deaktivieren",disable_overpowering:"Überverbrauch deaktivieren",disable_heat:"Heizen deaktivieren",disable_cool:"Kühlen deaktivieren",disable_heat_cool:"Heizen/Kühlen deaktivieren",disable_auto:"Auto deaktivieren",disable_fan_only:"Nur-Lüfter deaktivieren",disable_dry:"Entfeuchten deaktivieren",disable_off:"Aus deaktivieren",disable_menu:"Menü deaktivieren",set_current_as_main:"Zieltemperatur mit aktueller Temperatur tauschen",disable_safety_warning:"Sicherheitsmeldungen deaktivieren",disable_buttons:"Plus/Minus-Buttons deaktivieren",disable_power_infos:"Verbrauchsinformationen deaktivieren",disable_auto_fan_infos:"Auto-Lüfter-Informationen deaktivieren",disable_name:"Name deaktivieren",eco_temperature:"Eco-Temperatur",allow_lock_toggle:"Allow lock/unlock from card",menu_system:"System menu",disable_sleep:"Disable sleep mode",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Voreinstellungstemperaturen bearbeitbar machen",show_status_text:"Aktuellen Status als Text anzeigen",show_background_gradient_on_active:"Hintergrundverlauf des Schiebereglers anzeigen, wenn die Entität aktiv ist",show_regulation_chart:"Show regulation chart"}}},Yt={window_open:"Fenster offen",window_bypass:"Fenster-Bypass aktiviert",night_mode:"Nachtmodus",eco:"Eco",overpowering:"Überverbrauch erkannt",none:"Manuell",comfort:"Komfort",boost:"Boost",frost:"Frostschutz",power:"Lastabwurf",activity:"Aktivität erkannt",presence:"Anwesenheit erkannt",motion:"Bewegung erkannt",auto_regulation_light:"Leicht",auto_regulation_medium:"Mittel",auto_regulation_strong:"Stark",auto_regulation_slow:"Langsam",auto_regulation_expert:"Experte",auto_fan_mode:"Auto-Lüfter",auto_fan_mode_off:"Keine",auto_fan_none:"Keine",auto_fan_low:"Niedrig",auto_fan_medium:"Mittel",auto_fan_high:"Hoch",auto_fan_turbo:"Turbo",fan_low:"Niedrig",fan_low_mid:"Niedrig-mittel",fan_mid:"Mittel",fan_mid_high:"Mittel-hoch",fan_high:"Hoch",fan_mute:"Leise",fan_turbo:"Turbo",fan_none:"Keine",fan_auto:"Auto",change_message:"Klicken zum Ändern des Presets",toggle_message:"Klicken zum Umschalten",auto_start_stop:"Gestoppt durch Auto-Start/Stop",auto_start_stop_enable:"Aktiviert/deaktiviert Auto-Start/Stop",auto_start_stop_label:"Auto-Start/Stop",temp_unavailable:"-",sleep:"Schlafmodus",lock_users_active:"Benutzersteuerung gesperrt",lock_automations_active:"Automatisierungsänderungen gesperrt",lock:"Sperren",unlock:"Entsperren",code_error:"Falscher Code",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",increase_temp:"Temperatur erhöhen",decrease_temp:"Temperatur verringern",undefined:"None",auto_fan_boost:"Boost",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",not_initialized:"Initialisation error",preset_mod_title:"Voreinstellungstemperaturen",preset_mod_no_entities:"Keine Voreinstellungstemperatur-Entitäten für dieses Gerät gefunden.",preset_row_heat:"Heizen",preset_row_heat_away:"Heizen (abwesend)",preset_row_cool:"Kühlen",preset_row_cool_away:"Kühlen (abwesend)",regulation_chart_title:"Regulation chart"},qt={common:jt,editor:Ut,extra_states:Yt},Zt=Object.freeze({__proto__:null,common:jt,default:qt,editor:Ut,extra_states:Yt}),Xt={version:"version",current:"Actuelle"},Kt={card:{climate:{autoStartStopEnableEntity:"Entité - auto-start/stop",powerEntity:"Entité - puissance consommée",theme:"Thème",theme_classic:"Classique",theme_vtherm:"VTherm",theme_uncolored:"Sans couleur",theme_gunmalmg:"Gunmalmg",section_all_themes:"Pour tous les thèmes",section_classic_only:"Pour les thèmes Classique, VTherm et Sans couleur",menu_system:"Menu système",disable_window:"Désactiver fenêtre status",disable_autoStartStop:"Désactiver auto-startStop",disable_overpowering:"Désactiver sur-puissance status",disable_heat:"Désactiver mode chauffe",disable_cool:"Désactiver mode clim",disable_heat_cool:"Désactiver mode chauffe/clim",disable_auto:"Désactiver mode auto",disable_fan_only:"Désactiver mode ventilation",disable_dry:"Désactiver mode déshumidification",disable_off:"Désactiver mode arret",disable_sleep:"Désactiver mode endormi",set_current_as_main:"Echanger temperature cible avec temperature locale",disable_safety_warning:"Désactiver sécurité messages",disable_buttons:"Désactiver les boutons +/-",disable_power_infos:"Désactiver les infos de puissance",disable_auto_fan_infos:"Désactiver les infos auto-ventilation",disable_name:"Désactiver le nom",disable_target_icon:"Désactiver l'icône cible",allow_lock_toggle:"Activer le verrouillage/déverrouillage depuis la carte",disable_presets:"Désactiver les presets",disable_timed_preset:"Désactiver le champ de durée du preset temporisé",use_manual_duration_input:"Permettre la saisie manuelle de la durée",allow_preset_modification:"Permettre la modification des températures de preset",disable_menu:"Désactiver le menu",show_status_text:"Afficher l'état du HVAC en texte",show_background_gradient_on_active:"Afficher le dégradé de fond du slider lorsque l'entité est active",show_regulation_chart:"Afficher le graphique de régulation"}}},Gt={window_open:"Fenêtre ouverte",window_bypass:"Bypass fenêtre activé",eco:"Eco",overpowering:"Sur-puissance détectée",none:"Manuel",comfort:"Confort",boost:"Boost",frost:"Hors-gel",power:"Délestage",activity:"Activité",presence:"Présence détectée",motion:"Mouvement détecté",auto_regulation_light:"Légère",auto_regulation_medium:"Moyenne",auto_regulation_strong:"Forte",auto_regulation_slow:"Lente",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"Aucun",auto_fan_none:"Aucun",auto_fan_low:"Bas",auto_fan_medium:"Moyen",auto_fan_high:"Haut",auto_fan_turbo:"Turbo",fan_low:"Bas",fan_low_mid:"Bas-moyen",fan_mid:"Moyen",fan_mid_high:"Moyen-haut",fan_high:"Haut",fan_mute:"Silence",fan_turbo:"Turbo",fan_none:"Aucun",fan_auto:"Auto",fan_1:"Vitesse 1",fan_2:"Vitesse 2",fan_3:"Vitesse 3",fan_4:"Vitesse 4",fan_5:"Vitesse 5",change_message:"Clic pour sélectionner le preset",toggle_message:"Clic pour basculer",auto_start_stop:"Stoppé par auto-start/stop",auto_start_stop_enable:"Active/désactive l'auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Endormi","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Temp. pièce",outdoor_temp:"Temp. ext.",safety_warning:"Attention",safety_warning_msg:"défaut de temp.",hvac_off_manual:"Eteint manuellement",hvac_off_auto_start_stop:"Eteint par auto-start/stop",hvac_off_window_detection:"Eteint par détection d'ouverture",hvac_off_sleep_mode:"Eteint par le mode endormi",hvac_off_safety_detection:"Eteint par le mode sécurité",hvac_off_central_mode:"Eteint par le mode central",safety_detected:"Mode sécurité détectée",overpowering_detected:"Sur-puissance détectée",target_temp_window_eco:"Temp. cible : fenêtre éco",target_temp_window_frost:"Temp. cible : fenêtre hors-gel",target_temp_power:"Temp. cible : délestage",target_temp_central_mode:"Temp. cible : mode central",target_temp_activity_detected:"Temp. cible : activité détectée",target_temp_activity_not_detected:"Temp. cible : activité non détectée",target_temp_absence_detected:"Temp. cible : absence détectée",target_temp_timed_preset:"Temporisation d'un preset active",lock_users_active:"Verrouillage utilisateurs actif",lock_automations_active:"Verrouillage automations actif","messages-button":"Pourquoi cet état ?",recalculation_scheduled:"Sous-jacent en cours de mise à jour","window-bypass-button":"Basculer le bypass fenêtre",code_error:"Code incorrect",lock:"Verrouiller",unlock:"Déverrouiller",timed_preset_title:"Durée en minutes (0 = permanent)",timed_preset_active:"Preset temporisé actif",cancel_timed_preset:"Annuler le preset temporisé",heating_failure:"Défaut de chauffage détecté",cooling_failure:"Défaut de refroidissement détecté",increase_temp:"Augmenter la température",decrease_temp:"Diminuer la température",night_mode:"Mode nuit",undefined:"Aucun",auto_fan_boost:"Boost",not_initialized:"Erreur d'initialisation",preset_mod_title:"Températures des presets",preset_mod_no_entities:"Aucune entité de température de preset trouvée pour cet appareil.",preset_row_heat:"Chauffe",preset_row_heat_away:"Chauffe absent",preset_row_cool:"Clim.",preset_row_cool_away:"Clim. absent",regulation_chart_title:"Graphique de régulation"},Qt={common:Xt,editor:Kt,extra_states:Gt},Jt=Object.freeze({__proto__:null,common:Xt,default:Qt,editor:Kt,extra_states:Gt}),te={version:"Версия",current:"Текущая"},ee={card:{climate:{autoStartStopEnableEntity:"Сущность автоматического старта/стопа",powerEntity:"Сущность потребления энергии",disable_window:"Отключить окно",disable_autoStartStop:"Отключить автоматический старт/стоп",disable_overpowering:"Отключить перегрузку",disable_heat:"Отключить обогрев",disable_cool:"Отключить охлаждение",disable_heat_cool:"Отключить обогрев/охлаждение",disable_auto:"Отключить авто",disable_fan_only:"Отключить только вентилятор",disable_dry:"Отключить осушение",disable_off:"Отключить режим выключения",disable_sleep:"Отключить спящий режим",disable_menu:"Отключить меню",set_current_as_main:"Поменять местами целевую и текущую температуру",disable_safety_warning:"Отключить сообщения безопасности",disable_buttons:"Отключить кнопки плюс/минус",disable_power_infos:"Отключить информацию о питании",disable_auto_fan_infos:"Отключить информацию автовентилятора",disable_name:"Отключить название",disable_target_icon:"Отключить иконку цели",allow_lock_toggle:"Разрешить блокировку/разблокировку с карты",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Разрешить изменение температур пресетов",show_status_text:"Показывать текущий статус в виде текста",show_background_gradient_on_active:"Показывать фоновый градиент ползунка, когда сущность активна",show_regulation_chart:"Show regulation chart"}}},ie={window_open:"Окно открыто",window_bypass:"Обход окна активирован",night_mode:"Ночной режим",eco:"Эко",overpowering:"Обнаружена перегрузка",none:"Ручной",comfort:"Комфорт",boost:"Турбо",frost:"Защита от замерзания",power:"Перегрузка",activity:"Обнаружена активность",presence:"Обнаружено присутствие",motion:"Обнаружено движение",undefined:"Нет",auto_regulation_light:"Легкий",auto_regulation_medium:"Средний",auto_regulation_strong:"Сильный",auto_regulation_slow:"Медленный",auto_regulation_expert:"Экспертный",auto_fan_mode:"Авторежим вентилятора",auto_fan_mode_off:"Нет",auto_fan_none:"Нет",auto_fan_low:"Низкий",auto_fan_medium:"Средний",auto_fan_high:"Высокий",auto_fan_boost:"Турбо",fan_low:"Низкий",fan_low_mid:"Низкий-средний",fan_mid:"Средний",fan_mid_high:"Средний-высокий",fan_high:"Высокий",fan_mute:"Тихий",fan_turbo:"Турбо",fan_none:"Нет",fan_auto:"Авто",change_message:"Клик для смены предустановки",toggle_message:"Клик для переключения",auto_start_stop:"Остановлено авто-старт/стопом",auto_start_stop_enable:"Включить/отключить авто-старт/стоп",auto_start_stop_label:"Авто-старт/стоп",temp_unavailable:"-",sleep:"Сон",minutes:"мин",room_temp:"Температура в помещении",outdoor_temp:"Уличная температура",lock_users_active:"Пользовательское управление заблокировано",lock_automations_active:"Изменения автоматизации заблокированы",code_error:"Неверный код",lock:"Блокировать",unlock:"Разблокировать",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5","New strings for recent edits. Please translate":"-------------------------------------",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Температуры пресетов",preset_mod_no_entities:"Не найдено объектов температуры пресетов для этого устройства.",preset_row_heat:"Обогрев",preset_row_heat_away:"Обогрев (отсутствие)",preset_row_cool:"Охлаждение",preset_row_cool_away:"Охлаждение (отсутствие)",regulation_chart_title:"Regulation chart"},oe="Тема",ne="Классический",ae="VTherm",se="Без цвета",re="Gunmalmg",le={common:te,editor:ee,extra_states:ie,theme:oe,theme_classic:ne,theme_vtherm:ae,theme_uncolored:se,theme_gunmalmg:re},ce=Object.freeze({__proto__:null,common:te,default:le,editor:ee,extra_states:ie,theme:oe,theme_classic:ne,theme_gunmalmg:re,theme_uncolored:se,theme_vtherm:ae}),de={version:"wersja",current:"aktualna"},he={card:{climate:{allow_lock_toggle:"Zezwalaj na blokowanie/odblokowanie karty",autoStartStopEnableEntity:"Encja autoSTART/autoSTOP",powerEntity:"Encja poboru mocy",disable_window:"Wyłącz okno",disable_autoStartStop:"Wyłącz autoSTART/autoSTOP",disable_overpowering:"Wyłącz przeciążenie",disable_heat:"Wyłącz tryb grzania",disable_cool:"Wyłącz tryb chłodzenia",disable_heat_cool:"Wyłącz tryb 'grzanie/chłodzenie'",disable_auto:"Wyłącz tryb automatyczny",disable_fan_only:"Wyłącz tryb wentylatora",disable_dry:"Wyłącz tryb osuszania",disable_off:"Wyłącz tryb 'OFF'",disable_sleep:"Wyłącz tryb uśpienia",disable_menu:"Wyłącz menu",set_current_as_main:"Zamień temperaturę docelową z temperaturą pomieszczenia",disable_safety_warning:"Wyłącz ostrzeżenie bezpieczeństwa",disable_buttons:"Wyłącz przyciski +/- (plus/minus)",disable_power_infos:"Wyłącz komunikaty mocy",disable_auto_fan_infos:"Wyłącz komunikaty autowentylacji",disable_name:"Wyłącz nazwę",disable_target_icon:"Wyłącz ikonę",theme:"Motyw",theme_classic:"Klasyczny",theme_vtherm:"VTherm",theme_uncolored:"Bez koloru",theme_gunmalmg:"Gunmalmg",section_all_themes:"Dla wszystkich motywów",section_classic_only:"Dla motywu klasycznego, bez koloru i VTherm",menu_system:"Menu systemowe",disable_presets:"Wyłącz presety",disable_timed_preset:"Wyłącz czas trwania presetu",use_manual_duration_input:"Zamiast selektora zezwól na ręczne ustawienie czasu",allow_preset_modification:"Zezwól na modyfikację temperatur ustawień wstępnych",show_status_text:"Pokaż aktualny status jako tekst",show_background_gradient_on_active:"Pokaż gradient tła suwaka, gdy encja jest aktywna",show_regulation_chart:"Show regulation chart"}}},ue={window_open:"Otwarte okno",window_bypass:"Załączone pomijanie okien",night_mode:"Tryb nocny",eco:"Tryb Eko",overpowering:"Wykryte przeciążenie",none:"Ręczny",theme:"Motyw",theme_classic:"Klasyczny",theme_vtherm:"VTherm",theme_uncolored:"Bez koloru",theme_gunmalmg:"Gunmalmg",comfort:"Komfort",boost:"Boost",frost:"Ochrona przed mrozem",power:"Przeciążenie",activity:"Wykryto aktywność",presence:"Wykryto obecność",motion:"Wykryto ruch",undefined:"Brak",auto_regulation_light:"Słaby",auto_regulation_medium:"Średni",auto_regulation_strong:"Silny",auto_regulation_slow:"Powolny",auto_regulation_expert:"Ekspert",auto_fan_mode:"Autowentylacja",auto_fan_mode_off:"Wyłączony",auto_fan_none:"Brak",auto_fan_low:"Niski",auto_fan_medium:"Średni",auto_fan_high:"Wysoki",auto_fan_boost:"Boost",fan_low:"Niski",fan_low_mid:"Niskośredni",fan_mid:"Średni",fan_mid_high:"Średniowysoki",fan_high:"Wysoki",fan_mute:"Cichy",fan_turbo:"Turbo",fan_none:"Brak",fan_auto:"Auto",change_message:"Kliknij aby zmienić preset",toggle_message:"Kliknij aby przełączyć",auto_start_stop:"Zatrzymany przez autoSTART/autoSTOP",auto_start_stop_enable:"Załącz/wyłącz autoSTART/autoSTOP",auto_start_stop_label:"AutoSTART/autoSTOP",temp_unavailable:"-",sleep:"Uśpienie","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Temp. pomieszczenia",outdoor_temp:"Temp. zewnętrzna",safety_warning:"Ostrzeżenie",safety_warning_msg:"Błąd temperatury",hvac_off_manual:"Wyłączony ręcznie",hvac_off_auto_start_stop:"Wyłączony przez autoSTART/autoSTOP",hvac_off_window_detection:"Wyłączony prze otwarcie okna",hvac_off_sleep_mode:"Wyłączony przez tryb uśpienia",hvac_off_safety_detection:"Wyłączony przez tryb bezpieczny",hvac_off_central_mode:"Wyłączony przez tryb główny",safety_detected:"Wykryty tryb bezpieczny",overpowering_detected:"Wykryte przeciążenie",target_temp_window_eco:"Docelowa temp.: Eko okno",target_temp_window_frost:"Docelowa temp.: ochr. przed mrozem okno",target_temp_power:"Docelowa temp.: moc",target_temp_central_mode:"Docelowa temp.: tryb główny",target_temp_activity_detected:"Docelowa temp.: wykryta aktywność",target_temp_activity_not_detected:"Docelowa temp.: brak aktywności",target_temp_absence_detected:"Docelowa temp.: wykryta nieobecność","messages-button":"Dlaczego taki stan?",recalculation_scheduled:"Aktualizacja urządzeń w toku","window-bypass-button":"Przełącz pomijanie okna",lock_users_active:"Sterowanie użytkownika zablokowane",lock_automations_active:"Zmiany automatyzacji zablokowane",code_error:"Błędny kod",lock:"Zablokuj",unlock:"Odblokuj",heating_failure:"Wykryto błąd ogrzewania",cooling_failure:"Wykryto błąd chłodzenia",fan_1:"Prędkość 1",fan_2:"Prędkość 2",fan_3:"Prędkość 3",fan_4:"Prędkość 4",fan_5:"Prędkość 5",target_temp_timed_preset:"Aktywny preset czasowy",timed_preset_title:"Czas trwania (w min., 0 = stały)",timed_preset_active:"Aktywny preset czasowy",cancel_timed_preset:"Usuń preset czasowy",increase_temp:"Zwiększ temperaturę",decrease_temp:"Obniż temperaturę",not_initialized:"Błąd inicjalizacji",preset_mod_title:"Temperatury ustawień wstępnych",preset_mod_no_entities:"Nie znaleziono jednostek temperatury ustawień wstępnych dla tego urządzenia.",preset_row_heat:"Ogrzewanie",preset_row_heat_away:"Ogrzewanie (nieobecność)",preset_row_cool:"Chłodzenie",preset_row_cool_away:"Chłodzenie (nieobecność)",regulation_chart_title:"Regulation chart"},me={common:de,editor:he,extra_states:ue},pe=Object.freeze({__proto__:null,common:de,default:me,editor:he,extra_states:ue}),_e={version:"verzia",current:"aktuálny"},ge={card:{climate:{disable_window:"Zakázať okno",disable_overpowering:"Zakázať leto",disable_heat:"Zakázať kúrenie",disable_cool:"Zakázať chladenie",disable_heat_cool:"Zakázať kúrenie/chladenie",disable_auto:"Zakázat automatic",disable_fan_only:"Zakázať režim ventilátora",disable_dry:"Zakázať sušenie",disable_off:"Vypnúť",disable_menu:"Zakázať menu",disable_safety_warning:"Zakázať bezpečnostné správy",disable_buttons:"Zakázať plus/mínus tlačidlá",eco_temperature:"Eco teplota",set_current_as_main:"Vymeňte cieľ za miesta s aktuálnou teplotou",allow_lock_toggle:"Povoliť uzamknutie/odomknutie z karty",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_autoStartStop:"Disable auto-startStop",disable_sleep:"Disable sleep mode",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Povoliť úpravu teplôt predvolieb",show_status_text:"Zobraziť aktuálny stav textom",show_background_gradient_on_active:"Zobraziť prechod pozadia posúvača, keď je entita aktívna",show_regulation_chart:"Show regulation chart"}}},fe={window_open:"Okno otvorené",night_mode:"Nočný mód",eco:"Eco",overpowering:"Leto",lock_users_active:"Ovládanie používateľa zamknuté",lock_automations_active:"Zmeny automatizácie zamknuté",code_error:"Chybný kód",lock:"Zamknúť",unlock:"Odomknúť",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Teploty predvolieb",preset_mod_no_entities:"Pre toto zariadenie neboli nájdené žiadne entity teplôt predvolieb.",preset_row_heat:"Kúrenie",preset_row_heat_away:"Kúrenie (neprítomnosť)",preset_row_cool:"Chladenie",preset_row_cool_away:"Chladenie (neprítomnosť)",regulation_chart_title:"Regulation chart"},be="Téma",ve="Klasický",ye="VTherm",we="Bez farby",xe="Gunmalmg",ke={common:_e,editor:ge,extra_states:fe,theme:be,theme_classic:ve,theme_vtherm:ye,theme_uncolored:we,theme_gunmalmg:xe},Te={version:"Verzió",current:"Aktuális"},Se={card:{climate:{disable_window:"Ablak kikapcsolás",disable_overpowering:"Nyár kikapcsolás",disable_heat:"Fűtés kikacsolás",disable_cool:"Hűtés kikapcsolás",disable_heat_cool:"Fűtés/Hűtés kikapcsolás",disable_auto:"Auto kikapcsolás",disable_fan_only:"Csak ventillátor kikapcsolás",disable_dry:"Szárítás kikapcsolás",disable_off:"Kikapcsolás inaktiválás",eco_temperature:"Eco hőmérséklet",set_current_as_main:"Aktuális hőmérséklet használata",allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_autoStartStop:"Disable auto-startStop",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Előbeállítási hőmérsékletek módosításának engedélyezése",show_status_text:"Aktuális állapot megjelenítése szövegként",show_background_gradient_on_active:"A csúszka háttérszínátmenetének megjelenítése, ha az entitás aktív",show_regulation_chart:"Show regulation chart"}}},Me={window_open:"Ablak nyitva",night_mode:"Éjszakai mód",eco:"Eco",overpowering:"Nyár",lock_users_active:"Felhasználói vezérlők zárolva",lock_automations_active:"Automatizálási változtatások zárolva",code_error:"Rossz kód",lock:"Zárolás",unlock:"Feloldás",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Előbeállítási hőmérsékletek",preset_mod_no_entities:"Nem találhatók előbeállítási hőmérséklet entitások ehhez az eszközhöz.",preset_row_heat:"Fűtés",preset_row_heat_away:"Fűtés (távol)",preset_row_cool:"Hűtés",preset_row_cool_away:"Hűtés (távol)",regulation_chart_title:"Regulation chart"},De="Téma",Ce="Klasszikus",Ae="VTherm",Pe="Színtelen",Ee="Gunmalmg",Le={common:Te,editor:Se,extra_states:Me,theme:De,theme_classic:Ce,theme_vtherm:Ae,theme_uncolored:Pe,theme_gunmalmg:Ee},$e={version:"version",current:"nuværende"},ze={card:{climate:{allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Tillad ændring af forudindstillingstemperaturer",show_status_text:"Vis aktuel status som tekst",show_background_gradient_on_active:"Vis skyderens baggrundsgradient, når enheden er aktiv",show_regulation_chart:"Show regulation chart"}}},Oe={window_open:"Vindue åben",night_mode:"Nattilstand",eco:"Eco",overpowering:"Overpowering",lock_users_active:"Brugerkontroller låst",lock_automations_active:"Automatiske ændringer låst",code_error:"Forkert kode",lock:"Lås",unlock:"Lås op",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Forudindstilte temperaturer",preset_mod_no_entities:"Ingen forudindstillede temperaturenheder fundet for denne enhed.",preset_row_heat:"Varme",preset_row_heat_away:"Varme fraværende",preset_row_cool:"Køling",preset_row_cool_away:"Køling fraværende",regulation_chart_title:"Regulation chart"},Ve="Tema",He="Klassisk",Re="VTherm",Ie="Ufarvet",Ne="Gunmalmg",Fe={common:$e,editor:ze,extra_states:Oe,theme:Ve,theme_classic:He,theme_vtherm:Re,theme_uncolored:Ie,theme_gunmalmg:Ne},We={version:"version",current:"Actual"},Be={card:{climate:{allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",show_status_text:"Mostrar el estado actual en texto",show_background_gradient_on_active:"Mostrar el degradado de fondo del control deslizante cuando la entidad está activa",allow_preset_modification:"Allow preset temperature modification",show_regulation_chart:"Show regulation chart"}}},je={window_open:"Ventana abierta",night_mode:"Modo noche",eco:"Eco",overpowering:"Verano",lock_users_active:"Controles de usuario bloqueados",lock_automations_active:"Cambios de automatización bloqueados",code_error:"Código incorrecto",lock:"Bloquear",unlock:"Desbloquear",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",increase_temp:"Aumentar la temperatura",decrease_temp:"Disminuir la temperatura",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",not_initialized:"Initialisation error",preset_mod_title:"Temperaturas de presets",preset_mod_no_entities:"No se encontraron entidades de temperatura de preset para este dispositivo.",preset_row_heat:"Calefacción",preset_row_heat_away:"Calefacción ausente",preset_row_cool:"Refrigeración",preset_row_cool_away:"Refrigeración ausente",regulation_chart_title:"Regulation chart"},Ue="Tema",Ye="Clásico",qe="VTherm",Ze="Sin color",Xe="Gunmalmg",Ke={common:We,editor:Be,extra_states:je,theme:Ue,theme_classic:Ye,theme_vtherm:qe,theme_uncolored:Ze,theme_gunmalmg:Xe},Ge={version:"versiyon",current:"şimdiki"},Qe={card:{climate:{allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Ön ayar sıcaklıklarının değiştirilmesine izin ver",show_status_text:"Mevcut durumu metin olarak göster",show_background_gradient_on_active:"Varlık etkinken kaydırıcı arka plan gradyanını göster",show_regulation_chart:"Show regulation chart"}}},Je={window_open:"Pencere açık",night_mode:"Gece modu",eco:"Eco",overpowering:"Yaz",lock_users_active:"Kullanıcı kontrolleri kilitlendi",lock_automations_active:"Otomasyon değişiklikleri kilitlendi",code_error:"Hatalı kod",lock:"Kilitle",unlock:"Kilidi aç",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Ön ayar sıcaklıkları",preset_mod_no_entities:"Bu cihaz için ön ayar sıcaklığı varlıkları bulunamadı.",preset_row_heat:"Isıtma",preset_row_heat_away:"Isıtma (uzakta)",preset_row_cool:"Soğutma",preset_row_cool_away:"Soğutma (uzakta)",regulation_chart_title:"Regulation chart"},ti="Tema",ei="Klasik",ii="VTherm",oi="Renksiz",ni="Gunmalmg",ai={common:Ge,editor:Qe,extra_states:Je,theme:ti,theme_classic:ei,theme_vtherm:ii,theme_uncolored:oi,theme_gunmalmg:ni},si={version:"versione",current:"attuale"},ri={card:{climate:{autoStartStopEnableEntity:"Entità di avvio/arresto automatico",powerEntity:"Entità di consumo energetico",theme:"Tema",theme_classic:"Classico",theme_vtherm:"VTherm",theme_uncolored:"Senza colore",theme_gunmalmg:"Gunmalmg",section_all_themes:"Per tutti i temi",section_classic_only:"Per i temi Classic, VTherm e Uncolored",menu_system:"System menu",disable_window:"Disabilita la finestra",disable_autoStartStop:"Disabilita Avvio/Arresto automatico",disable_overpowering:"Disabilita la sovralimentazione",disable_heat:"Disabilita Riscalda",disable_cool:"Disabilita Raffresca",disable_heat_cool:"Disabilita Riscalda/Raffredda",disable_auto:"Disabilita Automatico",disable_fan_only:"Disabilita Ventilazione",disable_dry:"Disabilita Deumidifica",disable_off:"Disabilita off",disable_menu:"Disabilita menu",set_current_as_main:"Scambia Setpoint con Temperatura ambiente",disable_safety_warning:"Disabilita i messaggi di sicurezza",disable_buttons:"Disabilita pulsanti Più/Meno",disable_power_infos:"Disabilita Info Potenza",disable_auto_fan_infos:"Disabilita Info auto-fan",disable_name:"Disabilita nome",disable_target_icon:"Disabilita icona obbiettivo",allow_lock_toggle:"Consenti blocco/sblocco dalla card",disable_sleep:"Disabilita modo riposo",disable_timed_preset:"Disattiva immissione durata preimpostata",use_manual_duration_input:"Consenti l'inserimento manuale della durata invece della selezione",disable_presets:"Disabilita preimpostazioni",allow_preset_modification:"Consenti la modifica delle temperature dei preset",show_status_text:"Mostra lo stato attuale come testo",show_background_gradient_on_active:"Mostra il gradiente di sfondo del cursore quando l'entità è attiva",show_regulation_chart:"Show regulation chart"}}},li={window_open:"Finestra aperta",window_bypass:"Bypass finestra Attivo",eco:"Eco",overpowering:"Rilevata Sovrapotenza",none:"Manuale",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Sovrapotenza",activity:"Rilevata Attività",presence:"Rilevata Presenza",motion:"Rilevato movimento",auto_regulation_light:"Leggero",auto_regulation_medium:"Medio",auto_regulation_strong:"Forte",auto_regulation_slow:"Lento",auto_regulation_expert:"Esperto",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"Nulla",auto_fan_none:"Nulla",auto_fan_low:"Basso",auto_fan_medium:"Medio",auto_fan_high:"Alto",auto_fan_boost:"Boost",fan_low:"Basso",fan_low_mid:"Medio-basso",fan_mid:"Medio",fan_mid_high:"Medio-alto",fan_high:"Alto",fan_mute:"Silenzioso",fan_turbo:"Turbo",fan_none:"Nulla",fan_auto:"Auto",fan_1:"Velocità 1",fan_2:"Velocità 2",fan_3:"Velocità 3",fan_4:"Velocità 4",fan_5:"Velocità 5",change_message:"Click per cambio preimpostazione",toggle_message:"Click per commutare",auto_start_stop:"Fermato da auto-start/stop",auto_start_stop_enable:"Abilita/disabilita auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Riposo","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Temp. ambiente",outdoor_temp:"Temp. esterna",safety_warning:"Avviso",safety_warning_msg:"Guasto Temp.",hvac_off_manual:"Spento manualmente",hvac_off_auto_start_stop:"Spento da auto-start/stop",hvac_off_window_detection:"Spento da rilevamento finestra",hvac_off_sleep_mode:"Spento da modo notte",hvac_off_safety_detection:"Spento da modalità sicurezza",hvac_off_central_mode:"Spento da modalità centralizzato",safety_detected:"Modalità sicurezza rilavata",overpowering_detected:"Sovrapotenza rilevata",target_temp_window_eco:"Temp. impostata: window eco",target_temp_window_frost:"Temp. impostata: window freddo",target_temp_power:"Temp. impostata: power",target_temp_central_mode:"Temp. impostata: modalità centralizzata",target_temp_activity_detected:"Temp. impostata: rilevata atività",target_temp_activity_not_detected:"Temp. impostata: attività non rilevata",target_temp_absence_detected:"Temp. impostata: assenza rilevata",target_temp_timed_preset:"Preimpostazione a tempo attiva",lock_users_active:"Controlli utente bloccati",lock_automations_active:"Modifiche automazioni bloccate","messages-button":"Perchè questo stato?",recalculation_scheduled:"Aggiornamento sottostanti in corso","window-bypass-button":"Commuta bypass finestra",code_error:"Codice errato",lock:"Blocca",unlock:"Sblocca",timed_preset_title:"Durata in minuti (0 = permanente)",timed_preset_active:"Preimpostazione temporizzata attiva",cancel_timed_preset:"Disattiva Preimpostazione temporizzata",heating_failure:"Rilevato guasto al riscaldamento",cooling_failure:"Rilevato guasto al raffrescamento",increase_temp:"Aumenta la temperatura",decrease_temp:"Diminuisci la temperatura",night_mode:"Modalità notte",undefined:"Nulla",not_initialized:"Errore di inizializzazione",preset_mod_title:"Temperature dei preset",preset_mod_no_entities:"Nessuna entità di temperatura preset trovata per questo dispositivo.",preset_row_heat:"Riscaldamento",preset_row_heat_away:"Riscaldamento (assente)",preset_row_cool:"Raffreddamento",preset_row_cool_away:"Raffreddamento (assente)",regulation_chart_title:"Regulation chart"},ci={common:si,editor:ri,extra_states:li},di={version:"versão",current:"actual"},hi={card:{climate:{disable_window:"Desactivar Janela",disable_overpowering:"Desactivar Verão",disable_heat:"Desactivar Aquecimento",disable_cool:"Desactivar Arrefecimento",disable_heat_cool:"Desactivar Aquecimento/Arrefecimento",disable_auto:"Desactivar Auto",disable_fan_only:"Desactivar Ventilação",disable_dry:"Desactivar Secagem",disable_off:"Desactivar Off",eco_temperature:"Modo Eco",set_current_as_main:"Mudar para a temperatura local actual",theme:"Tema",theme_classic:"Clássico",theme_vtherm:"VTherm",theme_uncolored:"Sem cor",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",menu_system:"System menu",disable_autoStartStop:"Disable auto-startStop",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Permitir modificação das temperaturas de predefinição",show_status_text:"Mostrar o estado atual em texto",show_background_gradient_on_active:"Mostrar o gradiente de fundo do controle deslizante quando a entidade está ativa",show_regulation_chart:"Show regulation chart"}}},ui={window_open:"Janela Aberta",night_mode:"Modo Noturno",eco:"Eco",overpowering:"Verão",lock_users_active:"Controlos de utilizador bloqueados",lock_automations_active:"Alterações de automação bloqueadas",code_error:"Código incorreto",lock:"Bloquear",unlock:"Desbloquear",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Temperaturas das predefinições",preset_mod_no_entities:"Nenhuma entidade de temperatura de predefinição encontrada para este dispositivo.",preset_row_heat:"Aquecimento",preset_row_heat_away:"Aquecimento (ausente)",preset_row_cool:"Arrefecimento",preset_row_cool_away:"Arrefecimento (ausente)",regulation_chart_title:"Regulation chart"},mi={common:di,editor:hi,extra_states:ui},pi={version:"版本",current:"当前"},_i={card:{climate:{allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"允许修改预设温度",show_status_text:"以文本形式显示当前状态",show_background_gradient_on_active:"当实体处于活动状态时显示滑块背景渐变",show_regulation_chart:"Show regulation chart"}}},gi={window_open:"窗户打开",night_mode:"夜间模式",eco:"节能",overpowering:"夏季",lock_users_active:"用户控制已锁定",lock_automations_active:"自动化更改已锁定",code_error:"代码错误",lock:"锁定",unlock:"解锁",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"预设温度",preset_mod_no_entities:"未找到该设备的预设温度实体。",preset_row_heat:"制热",preset_row_heat_away:"制热(离开)",preset_row_cool:"制冷",preset_row_cool_away:"制冷(离开)",regulation_chart_title:"Regulation chart"},fi="VTherm",bi="Gunmalmg",vi={common:pi,editor:_i,extra_states:gi,theme:"主题",theme_classic:"经典",theme_vtherm:fi,theme_uncolored:"无颜色",theme_gunmalmg:bi},yi={version:"версія",current:"поточний"},wi={card:{climate:{allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Дозволити зміну температур попередніх налаштувань",show_status_text:"Показувати поточний статус у вигляді тексту",show_background_gradient_on_active:"Показувати фоновий градієнт повзунка, коли сутність активна",show_regulation_chart:"Show regulation chart"}}},xi={window_open:"Вікно відчинено",night_mode:"Нічний режим",eco:"Економія",overpowering:"Літо",lock_users_active:"Керування користувача заблоковано",lock_automations_active:"Зміни автоматизації заблоковані",code_error:"Невірний код",lock:"Блокувати",unlock:"Розблокувати",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Температури попередніх налаштувань",preset_mod_no_entities:"Не знайдено об'єктів температури попередніх налаштувань для цього пристрою.",preset_row_heat:"Опалення",preset_row_heat_away:"Опалення (відсутність)",preset_row_cool:"Охолодження",preset_row_cool_away:"Охолодження (відсутність)",regulation_chart_title:"Regulation chart"},ki="Тема",Ti="Класичний",Si="VTherm",Mi="Без кольору",Di="Gunmalmg",Ci={common:yi,editor:wi,extra_states:xi,theme:ki,theme_classic:Ti,theme_vtherm:Si,theme_uncolored:Mi,theme_gunmalmg:Di},Ai={version:"έκδοση",current:"τρέχουσα"},Pi={card:{climate:{allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Να επιτρέπεται η τροποποίηση θερμοκρασιών προεπιλογής",show_status_text:"Εμφάνιση τρέχουσας κατάστασης σε κείμενο",show_background_gradient_on_active:"Εμφάνιση της διαβάθμισης φόντου του ρυθμιστικού όταν η οντότητα είναι ενεργή",show_regulation_chart:"Show regulation chart"}}},Ei={window_open:"Παράθυρο ανοικτό",night_mode:"Λειτουργία νυκτός",eco:"Εξοικονόμηση",overpowering:"Καλοκαίρι",lock_users_active:"Έλεγχοι χρήστη κλειδωμένοι",lock_automations_active:"Αλλαγές αυτοματισμών κλειδωμένες",code_error:"Λάθος κωδικός",lock:"Κλείδωμα",unlock:"Ξεκλείδωμα",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Θερμοκρασίες προεπιλογής",preset_mod_no_entities:"Δεν βρέθηκαν οντότητες θερμοκρασίας προεπιλογής για αυτή τη συσκευή.",preset_row_heat:"Θέρμανση",preset_row_heat_away:"Θέρμανση (απουσία)",preset_row_cool:"Ψύξη",preset_row_cool_away:"Ψύξη (απουσία)",regulation_chart_title:"Regulation chart"},Li="Θέμα",$i="Κλασικό",zi="VTherm",Oi="Χωρίς χρώμα",Vi="Gunmalmg",Hi={common:Ai,editor:Pi,extra_states:Ei,theme:Li,theme_classic:$i,theme_vtherm:zi,theme_uncolored:Oi,theme_gunmalmg:Vi},Ri={version:"versie",current:"huidig"},Ii={card:{climate:{allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Voorkeursinstellingstemperaturen aanpassen toestaan",show_status_text:"Toon huidige status als tekst",show_background_gradient_on_active:"Toon achtergrondgradiënt van de schuifregelaar wanneer de entiteit actief is",show_regulation_chart:"Show regulation chart"}}},Ni={window_open:"Raam open",night_mode:"Nacht modus",eco:"Eco",overpowering:"Zomer",lock_users_active:"Gebruikersbediening vergrendeld",lock_automations_active:"Automatisatie wijzigingen vergrendeld",code_error:"Foute code",lock:"Vergrendelen",unlock:"Ontgrendelen",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Voorkeursinstellingstemperaturen",preset_mod_no_entities:"Geen entiteiten voor voorkeursinstellingstemperaturen gevonden voor dit apparaat.",preset_row_heat:"Verwarming",preset_row_heat_away:"Verwarming (afwezig)",preset_row_cool:"Koeling",preset_row_cool_away:"Koeling (afwezig)",regulation_chart_title:"Regulation chart"},Fi="Thema",Wi="Classic",Bi="VTherm",ji="Ongekleurd",Ui="Gunmalmg",Yi={common:Ri,editor:Ii,extra_states:Ni,theme:Fi,theme_classic:Wi,theme_vtherm:Bi,theme_uncolored:ji,theme_gunmalmg:Ui},qi={version:"versjon",current:"nåværende"},Zi={card:{climate:{allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Tillat endring av forhåndsinnstillingstemperaturer",show_status_text:"Vis gjeldende status som tekst",show_background_gradient_on_active:"Vis glidebryterens bakgrunnsgradient når enheten er aktiv",show_regulation_chart:"Show regulation chart"}}},Xi={window_open:"Vindu åpent",night_mode:"Nattmodus",eco:"Eco",overpowering:"Sommer",lock_users_active:"Brukerkontroller låst",lock_automations_active:"Automatiseringsendringer låst",code_error:"Feil kode",lock:"Lås",unlock:"Lås opp",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Forhåndsinnstillingstemperaturer",preset_mod_no_entities:"Ingen forhåndsinnstillingstemperaturenheter funnet for denne enheten.",preset_row_heat:"Varme",preset_row_heat_away:"Varme (fraværende)",preset_row_cool:"Kjøling",preset_row_cool_away:"Kjøling (fraværende)",regulation_chart_title:"Regulation chart"},Ki="Tema",Gi="Klassisk",Qi="VTherm",Ji="Uten farge",to="Gunmalmg",eo={common:qi,editor:Zi,extra_states:Xi,theme:Ki,theme_classic:Gi,theme_vtherm:Qi,theme_uncolored:Ji,theme_gunmalmg:to},io={version:"verze",current:"aktuální"},oo={card:{climate:{disable_window:"Zakázat okno",disable_overpowering:"Zakázat léto",disable_heat:"Zakázat topení",disable_cool:"Zakázat chlazení",disable_heat_cool:"Zakázat topení/chlazení",disable_auto:"Zakázať automatic",disable_fan_only:"Zakázat pouze ventilátor",disable_dry:"Zakázat sušení",disable_off:"Zakázat vypnuto",disable_menu:"Zakázat menu",disable_safety_warning:"Zakázat bezpečnostní oznámení",disable_buttons:"Zakázat tlačítka plus/minus",eco_temperature:"Eco teplota",set_current_as_main:"Prohodit místa cílovoé a aktáalní teploty",allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_autoStartStop:"Disable auto-startStop",disable_sleep:"Disable sleep mode",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Povolit úpravu teplot předvoleb",show_status_text:"Zobrazit aktuální stav textem",show_background_gradient_on_active:"Zobrazit přechod pozadí posuvníku, když je entita aktivní",show_regulation_chart:"Show regulation chart"}}},no={window_open:"okno otevřeno",night_mode:"Noční režim",eco:"Eco",overpowering:"Léto",lock_users_active:"Uživatelské ovládání zamčeno",lock_automations_active:"Změny automatizace zamčeny",code_error:"Chybný kód",lock:"Zamknout",unlock:"Odemknout",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Teploty předvoleb",preset_mod_no_entities:"Pro toto zařízení nebyly nalezeny žádné entity teplot předvoleb.",preset_row_heat:"Topení",preset_row_heat_away:"Topení nepřítomnost",preset_row_cool:"Chlazení",preset_row_cool_away:"Chlazení nepřítomnost",regulation_chart_title:"Regulation chart"},ao="Téma",so="Klasické",ro="VTherm",lo="Bez barvy",co="Gunmalmg",ho={common:io,editor:oo,extra_states:no,theme:ao,theme_classic:so,theme_vtherm:ro,theme_uncolored:lo,theme_gunmalmg:co},uo={version:"različica",current:"trenutno"},mo={card:{climate:{allow_lock_toggle:"Dovoli zaklepanje/odklepanje iz kartice",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Dovoli spremembo temperatur prednastavitev",show_status_text:"Prikaži trenutno stanje kot besedilo",show_background_gradient_on_active:"Prikaži preliv ozadja drsnika, ko je entiteta aktivna",show_regulation_chart:"Show regulation chart"}}},po={window_open:"Okno odprto",night_mode:"Nočni način",eco:"Eko",overpowering:"Poletje",lock_users_active:"Uporabniške kontrole zaklenjene",lock_automations_active:"Spremembe avtomatizacije zaklenjene",code_error:"Napačna koda",lock:"Zakleni",unlock:"Odkleni",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Temperature prednastavitev",preset_mod_no_entities:"Za to napravo ni bila najdena nobena entiteta temperature prednastavitve.",preset_row_heat:"Ogrevanje",preset_row_heat_away:"Ogrevanje (odsotnost)",preset_row_cool:"Hlajenje",preset_row_cool_away:"Hlajenje (odsotnost)",regulation_chart_title:"Regulation chart"},_o="Tema",go="Klasična",fo="VTherm",bo="Brez barve",vo="Gunmalmg",yo={common:uo,editor:mo,extra_states:po,theme:_o,theme_classic:go,theme_vtherm:fo,theme_uncolored:bo,theme_gunmalmg:vo},wo={version:"version",current:"Nuvarande"},xo={window_open:"Fönster öppet",night_mode:"Nattläge",eco:"Eco",overpowering:"Sommar",lock_users_active:"Användarkontroller låsta",lock_automations_active:"Automationsändringar låsta",code_error:"Fel kod",lock:"Lås",unlock:"Lås upp",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Förinställningstemperaturer",preset_mod_no_entities:"Inga förinställningstemperaturenheter hittades för den här enheten.",preset_row_heat:"Värme",preset_row_heat_away:"Värme (frånvaro)",preset_row_cool:"Kyla",preset_row_cool_away:"Kyla (frånvaro)",regulation_chart_title:"Regulation chart"},ko="Tema",To="Klassisk",So="VTherm",Mo="Omdfärgad",Do="Gunmalmg",Co={card:{climate:{autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",allow_lock_toggle:"Allow lock/unlock from card",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Tillåt ändring av förinställningstemperaturer",show_status_text:"Visa aktuell status som text",show_background_gradient_on_active:"Visa reglagets bakgrundsgradient när entiteten är aktiv",show_regulation_chart:"Show regulation chart"}}},Ao={common:wo,extra_states:xo,theme:ko,theme_classic:To,theme_vtherm:So,theme_uncolored:Mo,theme_gunmalmg:Do,editor:Co},Po={version:"версия",currrent:"текущий",current:"current"},Eo={card:{climate:{allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Позволи промяна на температурите на предварителни настройки",show_status_text:"Показване на статуса като текст",show_background_gradient_on_active:"Показване на фоновия градиент на плъзгача, когато обектът е активен",show_regulation_chart:"Show regulation chart"}}},Lo={window_open:"Отворен прозорец",night_mode:"Нощен режим",eco:"Екологичен режим",overpowering:"Лято",lock_users_active:"Потребителските контроли са заключени",lock_automations_active:"Промените от автоматизации са заключени",code_error:"Грешен код",lock:"Заключване",unlock:"Отключване",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Температури на предварителни настройки",preset_mod_no_entities:"Не са намерени обекти за температура на предварителни настройки за това устройство.",preset_row_heat:"Отопление",preset_row_heat_away:"Отопление (отсъствие)",preset_row_cool:"Охлаждане",preset_row_cool_away:"Охлаждане (отсъствие)",regulation_chart_title:"Regulation chart"},$o="Тема",zo="Класически",Oo="VTherm",Vo="Без цвят",Ho="Gunmalmg",Ro={common:Po,editor:Eo,extra_states:Lo,theme:$o,theme_classic:zo,theme_vtherm:Oo,theme_uncolored:Vo,theme_gunmalmg:Ho},Io={version:"version",current:"Nykyinen"},No={card:{climate:{allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Salli esiasetuslämpötilojen muokkaus",show_status_text:"Näytä nykyinen tila tekstinä",show_background_gradient_on_active:"Näytä liukusäätimen taustaliukuväri, kun entiteetti on aktiivinen",show_regulation_chart:"Show regulation chart"}}},Fo={window_open:"Ikkuna auki",night_mode:"Yötila",eco:"Eco",overpowering:"Kesä",lock_users_active:"Käyttäjän säädöt lukittu",lock_automations_active:"Automaation muutokset lukittu",code_error:"Virheellinen koodi",lock:"Lukitse",unlock:"Avaa lukitus",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Esiasetuslämpötilat",preset_mod_no_entities:"Tälle laitteelle ei löydetty esiasetuslämpötilaobjekteja.",preset_row_heat:"Lämmitys",preset_row_heat_away:"Lämmitys (poissa)",preset_row_cool:"Jäähdytys",preset_row_cool_away:"Jäähdytys (poissa)",regulation_chart_title:"Regulation chart"},Wo="Teema",Bo="Klassinen",jo="VTherm",Uo="Väritön",Yo="Gunmalmg",qo={common:Io,editor:No,extra_states:Fo,theme:Wo,theme_classic:Bo,theme_vtherm:jo,theme_uncolored:Uo,theme_gunmalmg:Yo},Zo={version:"versiune",current:"curent"},Xo={card:{climate:{allow_lock_toggle:"Permite blocarea/deblocarea din card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Permite modificarea temperaturilor presetate",show_status_text:"Afișează starea curentă ca text",show_background_gradient_on_active:"Afișează gradientul de fundal al glisorului când entitatea este activă",show_regulation_chart:"Show regulation chart"}}},Ko={window_open:"Fereastră deschisă",night_mode:"Mod noapte",eco:"Eco",overpowering:"Vară",lock_users_active:"Comenzi utilizator blocate",lock_automations_active:"Modificări automatizări blocate",code_error:"Cod incorect",lock:"Blocare",unlock:"Deblocare",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Temperaturi presetate",preset_mod_no_entities:"Nu s-au găsit entități de temperatură presetată pentru acest dispozitiv.",preset_row_heat:"Încălzire",preset_row_heat_away:"Încălzire (absent)",preset_row_cool:"Răcire",preset_row_cool_away:"Răcire (absent)",regulation_chart_title:"Regulation chart"},Go="Temă",Qo="Clasic",Jo="VTherm",tn="Fără culoare",en="Gunmalmg",on={common:Zo,editor:Xo,extra_states:Ko,theme:Go,theme_classic:Qo,theme_vtherm:Jo,theme_uncolored:tn,theme_gunmalmg:en},nn={version:"versió",current:"Actual"},an={card:{climate:{allow_lock_toggle:"Allow lock/unlock from card",autoStartStopEnableEntity:"Auto-start/stop entity",powerEntity:"Power consumption entity",theme:"Theme",theme_classic:"Classic",theme_vtherm:"VTherm",theme_uncolored:"Uncolored",theme_gunmalmg:"Gunmalmg",section_all_themes:"For all themes",section_classic_only:"For Classic, VTherm and Uncolored themes",menu_system:"System menu",disable_window:"Disable window",disable_autoStartStop:"Disable auto-startStop",disable_overpowering:"Disable overpowering",disable_heat:"Disable heat mode",disable_cool:"Disable cool mode",disable_heat_cool:"Disable heat/cool mode",disable_auto:"Disable auto mode",disable_fan_only:"Disable fan only mode",disable_dry:"Disable dry mode",disable_off:"Disable off mode",disable_sleep:"Disable sleep mode",disable_menu:"Disable menu",set_current_as_main:"Exchange target temperature and room temperature",disable_safety_warning:"Disable safety warning",disable_buttons:"Disable plus/minus buttons",disable_power_infos:"Disable power infos",disable_auto_fan_infos:"Disable auto-fan infos",disable_name:"Disable name",disable_target_icon:"Disable the target icon",disable_presets:"Disable presets",disable_timed_preset:"Disable timed preset duration input",use_manual_duration_input:"Allow manual duration input instead of selector",allow_preset_modification:"Permet la modificació de les temperatures de preset",show_status_text:"Mostra l'estat actual en text",show_background_gradient_on_active:"Mostra el degradat de fons del control lliscant quan l'entitat està activa",show_regulation_chart:"Show regulation chart"}}},sn={window_open:"Finestra oberta",night_mode:"Mode nocturn",eco:"Eco",overpowering:"Estiu",lock_users_active:"Controls d'usuari bloquejats",lock_automations_active:"Canvis d'automatització bloquejats",code_error:"Codi incorrecte",lock:"Bloquejar",unlock:"Desbloquejar",heating_failure:"Heating failure detected",cooling_failure:"Cooling failure detected",window_bypass:"Bypass window activated",none:"Manual",comfort:"Comfort",boost:"Boost",frost:"Frost",power:"Overpowering",activity:"Activity detected",presence:"Presence detected",motion:"Motion detected",undefined:"None",auto_regulation_light:"Light",auto_regulation_medium:"Medium",auto_regulation_strong:"Strong",auto_regulation_slow:"Slow",auto_regulation_expert:"Expert",auto_fan_mode:"Auto-fan",auto_fan_mode_off:"None",auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_boost:"Boost",fan_low:"Low",fan_low_mid:"Low-medium",fan_mid:"Medium",fan_mid_high:"Medium-high",fan_high:"High",fan_mute:"Mute",fan_turbo:"Turbo",fan_none:"None",fan_auto:"Auto",fan_1:"Speed 1",fan_2:"Speed 2",fan_3:"Speed 3",fan_4:"Speed 4",fan_5:"Speed 5",change_message:"Click to change preset",toggle_message:"Click to toggle",auto_start_stop:"Stopped by auto-start/stop",auto_start_stop_enable:"Enable/disable auto-start/stop",auto_start_stop_label:"Auto-start/stop",temp_unavailable:"-",sleep:"Sleeping","New strings for recent edits. Please translate":"-------------------------------------",minutes:"min",room_temp:"Room temp.",outdoor_temp:"Outdoor temp.",safety_warning:"Warning",safety_warning_msg:"Temp. fault",hvac_off_manual:"Turned off manually",hvac_off_auto_start_stop:"Turned off by auto-start/stop",hvac_off_window_detection:"Turned off by window detection",hvac_off_sleep_mode:"Turned off by sleep mode",hvac_off_safety_detection:"Turned off by safety mode",hvac_off_central_mode:"Turned off by central mode",safety_detected:"Safety mode detected",overpowering_detected:"Overpowering detected",target_temp_window_eco:"Target temp.: window eco",target_temp_window_frost:"Target temp.: window frost",target_temp_power:"Target temp.: power",target_temp_central_mode:"Target temp.: central mode",target_temp_activity_detected:"Target temp.: activity detected",target_temp_activity_not_detected:"Target temp.: activity not detected",target_temp_absence_detected:"Target temp.: absence detected",target_temp_timed_preset:"Timed preset active","messages-button":"Why this state?",recalculation_scheduled:"Underlying update in progress","window-bypass-button":"Toggle window bypass",timed_preset_title:"Duration in minutes (0 = permanent)",timed_preset_active:"Timed preset active",cancel_timed_preset:"Cancel timed preset",increase_temp:"Increase temperature",decrease_temp:"Decrease temperature",not_initialized:"Initialisation error",preset_mod_title:"Temperatures dels presets",preset_mod_no_entities:"No s'han trobat entitats de temperatura de preset per a aquest dispositiu.",preset_row_heat:"Calefacció",preset_row_heat_away:"Calefacció absent",preset_row_cool:"Refrigeració",preset_row_cool_away:"Refrigeració absent",regulation_chart_title:"Regulation chart"},rn="Tema",ln="Clàssic",cn="VTherm",dn="Sense color",hn="Gunmalmg",un={common:nn,editor:an,extra_states:sn,theme:rn,theme_classic:ln,theme_vtherm:cn,theme_uncolored:dn,theme_gunmalmg:hn};const mn={en:Bt,de:Zt,fr:Jt,ru:ce,sk:Object.freeze({__proto__:null,common:_e,default:ke,editor:ge,extra_states:fe,theme:be,theme_classic:ve,theme_gunmalmg:xe,theme_uncolored:we,theme_vtherm:ye}),hu:Object.freeze({__proto__:null,common:Te,default:Le,editor:Se,extra_states:Me,theme:De,theme_classic:Ce,theme_gunmalmg:Ee,theme_uncolored:Pe,theme_vtherm:Ae}),pl:pe,da:Object.freeze({__proto__:null,common:$e,default:Fe,editor:ze,extra_states:Oe,theme:Ve,theme_classic:He,theme_gunmalmg:Ne,theme_uncolored:Ie,theme_vtherm:Re}),es:Object.freeze({__proto__:null,common:We,default:Ke,editor:Be,extra_states:je,theme:Ue,theme_classic:Ye,theme_gunmalmg:Xe,theme_uncolored:Ze,theme_vtherm:qe}),tr:Object.freeze({__proto__:null,common:Ge,default:ai,editor:Qe,extra_states:Je,theme:ti,theme_classic:ei,theme_gunmalmg:ni,theme_uncolored:oi,theme_vtherm:ii}),it:Object.freeze({__proto__:null,common:si,default:ci,editor:ri,extra_states:li}),pt:Object.freeze({__proto__:null,common:di,default:mi,editor:hi,extra_states:ui}),cn:Object.freeze({__proto__:null,common:pi,default:vi,editor:_i,extra_states:gi,theme:"主题",theme_classic:"经典",theme_gunmalmg:bi,theme_uncolored:"无颜色",theme_vtherm:fi}),uk:Object.freeze({__proto__:null,common:yi,default:Ci,editor:wi,extra_states:xi,theme:ki,theme_classic:Ti,theme_gunmalmg:Di,theme_uncolored:Mi,theme_vtherm:Si}),el:Object.freeze({__proto__:null,common:Ai,default:Hi,editor:Pi,extra_states:Ei,theme:Li,theme_classic:$i,theme_gunmalmg:Vi,theme_uncolored:Oi,theme_vtherm:zi}),nl:Object.freeze({__proto__:null,common:Ri,default:Yi,editor:Ii,extra_states:Ni,theme:Fi,theme_classic:Wi,theme_gunmalmg:Ui,theme_uncolored:ji,theme_vtherm:Bi}),no:Object.freeze({__proto__:null,common:qi,default:eo,editor:Zi,extra_states:Xi,theme:Ki,theme_classic:Gi,theme_gunmalmg:to,theme_uncolored:Ji,theme_vtherm:Qi}),cs:Object.freeze({__proto__:null,common:io,default:ho,editor:oo,extra_states:no,theme:ao,theme_classic:so,theme_gunmalmg:co,theme_uncolored:lo,theme_vtherm:ro}),sl:Object.freeze({__proto__:null,common:uo,default:yo,editor:mo,extra_states:po,theme:_o,theme_classic:go,theme_gunmalmg:vo,theme_uncolored:bo,theme_vtherm:fo}),sv:Object.freeze({__proto__:null,common:wo,default:Ao,editor:Co,extra_states:xo,theme:ko,theme_classic:To,theme_gunmalmg:Do,theme_uncolored:Mo,theme_vtherm:So}),bg:Object.freeze({__proto__:null,common:Po,default:Ro,editor:Eo,extra_states:Lo,theme:$o,theme_classic:zo,theme_gunmalmg:Ho,theme_uncolored:Vo,theme_vtherm:Oo}),fi:Object.freeze({__proto__:null,common:Io,default:qo,editor:No,extra_states:Fo,theme:Wo,theme_classic:Bo,theme_gunmalmg:Yo,theme_uncolored:Uo,theme_vtherm:jo}),ro:Object.freeze({__proto__:null,common:Zo,default:on,editor:Xo,extra_states:Ko,theme:Go,theme_classic:Qo,theme_gunmalmg:en,theme_uncolored:tn,theme_vtherm:Jo}),ca:Object.freeze({__proto__:null,common:nn,default:un,editor:an,extra_states:sn,theme:rn,theme_classic:ln,theme_gunmalmg:hn,theme_uncolored:dn,theme_vtherm:cn})},pn="en";function _n({hass:t,string:e,search:i="",replace:o=""}){var n;const a=null!==(n=null==t?void 0:t.locale.language)&&void 0!==n?n:pn;let s;try{s=e.split(".").reduce(((t,e)=>t[e]),mn[a])}catch(t){s=e.split(".").reduce(((t,e)=>t[e]),mn.en)}return void 0===s&&(s=e.split(".").reduce(((t,e)=>t[e]),mn.en)),""!==i&&""!==o&&(s=s.replace(i,o)),s}function gn(t,e){try{return t.split(".").reduce(((t,e)=>t[e]),mn[e])}catch(t){return}}function fn(t){return function(e){var i;let o=gn(e,null!==(i=null==t?void 0:t.locale.language)&&void 0!==i?i:pn);return o||(o=gn(e,pn)),null!=o?o:e}}var bn,vn,yn=Number.isNaN||function(t){return"number"==typeof t&&t!=t};function wn(t,e){if(t.length!==e.length)return!1;for(var i=0;inew Intl.DateTimeFormat(t.language,{weekday:"long",month:"long",day:"numeric"}))),xn((t=>new Intl.DateTimeFormat(t.language,{year:"numeric",month:"long",day:"numeric"}))),xn((t=>new Intl.DateTimeFormat(t.language,{year:"numeric",month:"numeric",day:"numeric"}))),xn((t=>new Intl.DateTimeFormat(t.language,{day:"numeric",month:"short"}))),xn((t=>new Intl.DateTimeFormat(t.language,{month:"long",year:"numeric"}))),xn((t=>new Intl.DateTimeFormat(t.language,{month:"long"}))),xn((t=>new Intl.DateTimeFormat(t.language,{year:"numeric"}))),function(t){t.language="language",t.system="system",t.comma_decimal="comma_decimal",t.decimal_comma="decimal_comma",t.space_comma="space_comma",t.none="none"}(bn||(bn={})),function(t){t.language="language",t.system="system",t.am_pm="12",t.twenty_four="24"}(vn||(vn={}));const kn=xn((t=>{if(t.time_format===vn.language||t.time_format===vn.system){const e=t.time_format===vn.language?t.language:void 0,i=(new Date).toLocaleString(e);return i.includes("AM")||i.includes("PM")}return t.time_format===vn.am_pm}));xn((t=>new Intl.DateTimeFormat("en"!==t.language||kn(t)?t.language:"en-u-hc-h23",{year:"numeric",month:"long",day:"numeric",hour:kn(t)?"numeric":"2-digit",minute:"2-digit",hour12:kn(t)}))),xn((t=>new Intl.DateTimeFormat("en"!==t.language||kn(t)?t.language:"en-u-hc-h23",{year:"numeric",month:"long",day:"numeric",hour:kn(t)?"numeric":"2-digit",minute:"2-digit",second:"2-digit",hour12:kn(t)}))),xn((t=>new Intl.DateTimeFormat("en"!==t.language||kn(t)?t.language:"en-u-hc-h23",{year:"numeric",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit",hour12:kn(t)}))),xn((t=>new Intl.DateTimeFormat("en"!==t.language||kn(t)?t.language:"en-u-hc-h23",{hour:"numeric",minute:"2-digit",hour12:kn(t)}))),xn((t=>new Intl.DateTimeFormat("en"!==t.language||kn(t)?t.language:"en-u-hc-h23",{hour:kn(t)?"numeric":"2-digit",minute:"2-digit",second:"2-digit",hour12:kn(t)}))),xn((t=>new Intl.DateTimeFormat("en"!==t.language||kn(t)?t.language:"en-u-hc-h23",{weekday:"long",hour:kn(t)?"numeric":"2-digit",minute:"2-digit",hour12:kn(t)})));const Tn=(t,e,i,o)=>{o=o||{},i=null==i?{}:i;const n=new Event(e,{bubbles:void 0===o.bubbles||o.bubbles,cancelable:Boolean(o.cancelable),composed:void 0===o.composed||o.composed});return n.detail=i,t.dispatchEvent(n),n},Sn=(t,e,i)=>Math.min(Math.max(t,e),i),Mn=(t,e,i)=>{const o=e?(t=>{switch(t.number_format){case bn.comma_decimal:return["en-US","en"];case bn.decimal_comma:return["de","es","it"];case bn.space_comma:return["fr","sv","cs"];case bn.system:return;default:return t.language}})(e):void 0;if(Number.isNaN=Number.isNaN||function t(e){return"number"==typeof e&&t(e)},(null==e?void 0:e.number_format)!==bn.none&&!Number.isNaN(Number(t))&&Intl)try{return new Intl.NumberFormat(o,Dn(t,i)).format(Number(t))}catch(e){return console.error(e),new Intl.NumberFormat(void 0,Dn(t,i)).format(Number(t))}return"string"==typeof t?t:`${((t,e=2)=>Math.round(t*10**e)/10**e)(t,null==i?void 0:i.maximumFractionDigits).toString()}${"currency"===(null==i?void 0:i.style)?` ${i.currency}`:""}`},Dn=(t,e)=>{const i=Object.assign({maximumFractionDigits:2},e);if("string"!=typeof t)return i;if(!e||!e.minimumFractionDigits&&!e.maximumFractionDigits){const e=t.indexOf(".")>-1?t.split(".")[1].length:0;i.minimumFractionDigits=e,i.maximumFractionDigits=e}return i};class Cn extends TypeError{constructor(t,e){let i;const{message:o,explanation:n,...a}=t,{path:s}=t,r=0===s.length?o:`At path: ${s.join(".")} -- ${o}`;super(n??r),null!=n&&(this.cause=r),Object.assign(this,a),this.name=this.constructor.name,this.failures=()=>i??(i=[t,...e()])}}function An(t){return"object"==typeof t&&null!=t}function Pn(t){return"symbol"==typeof t?t.toString():"string"==typeof t?JSON.stringify(t):`${t}`}function En(t,e,i,o){if(!0===t)return;!1===t?t={}:"string"==typeof t&&(t={message:t});const{path:n,branch:a}=e,{type:s}=i,{refinement:r,message:l=`Expected a value of type \`${s}\`${r?` with refinement \`${r}\``:""}, but received: \`${Pn(o)}\``}=t;return{value:o,type:s,refinement:r,key:n[n.length-1],path:n,branch:a,...t,message:l}}function*Ln(t,e,i,o){(function(t){return An(t)&&"function"==typeof t[Symbol.iterator]})(t)||(t=[t]);for(const n of t){const t=En(n,e,i,o);t&&(yield t)}}function*$n(t,e,i={}){const{path:o=[],branch:n=[t],coerce:a=!1,mask:s=!1}=i,r={path:o,branch:n};if(a&&(t=e.coercer(t,r),s&&"type"!==e.type&&An(e.schema)&&An(t)&&!Array.isArray(t)))for(const i in t)void 0===e.schema[i]&&delete t[i];let l="valid";for(const o of e.validator(t,r))o.explanation=i.message,l="not_valid",yield[o,void 0];for(let[c,d,h]of e.entries(t,r)){const e=$n(d,h,{path:void 0===c?o:[...o,c],branch:void 0===c?n:[...n,d],coerce:a,mask:s,message:i.message});for(const i of e)i[0]?(l=null!=i[0].refinement?"not_refined":"not_valid",yield[i[0],void 0]):a&&(d=i[1],void 0===c?t=d:t instanceof Map?t.set(c,d):t instanceof Set?t.add(d):An(t)&&(void 0!==d||c in t)&&(t[c]=d))}if("not_valid"!==l)for(const o of e.refiner(t,r))o.explanation=i.message,l="not_refined",yield[o,void 0];"valid"===l&&(yield[void 0,t])}class zn{constructor(t){const{type:e,schema:i,validator:o,refiner:n,coercer:a=(t=>t),entries:s=function*(){}}=t;this.type=e,this.schema=i,this.entries=s,this.coercer=a,this.validator=o?(t,e)=>Ln(o(t,e),e,this,t):()=>[],this.refiner=n?(t,e)=>Ln(n(t,e),e,this,t):()=>[]}assert(t,e){return On(t,this,e)}create(t,e){return function(t,e,i){const o=Vn(t,e,{coerce:!0,message:i});if(o[0])throw o[0];return o[1]}(t,this,e)}is(t){return function(t,e){const i=Vn(t,e);return!i[0]}(t,this)}mask(t,e){return function(t,e,i){const o=Vn(t,e,{coerce:!0,mask:!0,message:i});if(o[0])throw o[0];return o[1]}(t,this,e)}validate(t,e={}){return Vn(t,this,e)}}function On(t,e,i){const o=Vn(t,e,{message:i});if(o[0])throw o[0]}function Vn(t,e,i={}){const o=$n(t,e,i),n=function(t){const{done:e,value:i}=t.next();return e?void 0:i}(o);if(n[0]){const t=new Cn(n[0],(function*(){for(const t of o)t[0]&&(yield t[0])}));return[t,void 0]}return[void 0,n[1]]}function Hn(t,e){return new zn({type:t,schema:null,validator:e})}function Rn(){return Hn("any",(()=>!0))}function In(t){return new zn({type:"array",schema:t,*entries(e){if(t&&Array.isArray(e))for(const[i,o]of e.entries())yield[i,o,t]},coercer:t=>Array.isArray(t)?t.slice():t,validator:t=>Array.isArray(t)||`Expected an array value, but received: ${Pn(t)}`})}function Nn(){return Hn("boolean",(t=>"boolean"==typeof t))}function Fn(t){const e=Pn(t),i=typeof t;return new zn({type:"literal",schema:"string"===i||"number"===i||"boolean"===i?t:null,validator:i=>i===t||`Expected the literal \`${e}\`, but received: ${Pn(i)}`})}function Wn(){return Hn("number",(t=>"number"==typeof t&&!isNaN(t)||`Expected a number, but received: ${Pn(t)}`))}function Bn(t){const e=t?Object.keys(t):[],i=Hn("never",(()=>!1));return new zn({type:"object",schema:t||null,*entries(o){if(t&&An(o)){const n=new Set(Object.keys(o));for(const i of e)n.delete(i),yield[i,o[i],t[i]];for(const t of n)yield[t,o[t],i]}},validator:t=>An(t)||`Expected an object, but received: ${Pn(t)}`,coercer:t=>An(t)?{...t}:t})}function jn(t){return new zn({...t,validator:(e,i)=>void 0===e||t.validator(e,i),refiner:(e,i)=>void 0===e||t.refiner(e,i)})}function Un(){return Hn("string",(t=>"string"==typeof t||`Expected a string, but received: ${Pn(t)}`))}function Yn(t){const e=Object.keys(t);return new zn({type:"type",schema:t,*entries(i){if(An(i))for(const o of e)yield[o,i[o],t[o]]},validator:t=>An(t)||`Expected an object, but received: ${Pn(t)}`,coercer:t=>An(t)?{...t}:t})}function qn(t){const e=t.map((t=>t.type)).join(" | ");return new zn({type:"union",schema:null,coercer(e){for(const i of t){const[t,o]=i.validate(e,{coerce:!0});if(!t)return o}return e},validator(i,o){const n=[];for(const e of t){const[...t]=$n(i,e,o),[a]=t;if(!a[0])return[];for(const[e]of t)e&&n.push(e)}return[`Expected the value to satisfy a union of \`${e}\`, but received: ${Pn(i)}`,...n]}})}const Zn=Bn({user:Un()}),Xn=qn([Nn(),Bn({text:jn(Un()),excemptions:jn(In(Zn))})]),Kn=Bn({action:Fn("url"),url_path:Un(),confirmation:jn(Xn)}),Gn=Bn({action:Fn("call-service"),service:Un(),service_data:jn(Bn()),data:jn(Bn()),target:jn(Bn({entity_id:jn(qn([Un(),In(Un())])),device_id:jn(qn([Un(),In(Un())])),area_id:jn(qn([Un(),In(Un())]))})),confirmation:jn(Xn)}),Qn=Bn({action:Fn("navigate"),navigation_path:Un(),confirmation:jn(Xn)}),Jn=Yn({action:Fn("fire-dom-event")}),ta=Bn({action:function(t){const e={},i=t.map((t=>Pn(t))).join();for(const i of t)e[i]=i;return new zn({type:"enums",schema:e,validator:e=>t.includes(e)||`Expected one of \`${i}\`, but received: ${Pn(e)}`})}(["none","toggle","more-info","call-service","url","navigate"]),confirmation:jn(Xn)});var ea;ea=t=>{if(t&&"object"==typeof t&&"action"in t)switch(t.action){case"call-service":return Gn;case"fire-dom-event":return Jn;case"navigate":return Qn;case"url":return Kn}return ta},new zn({type:"dynamic",schema:null,*entries(t,e){const i=ea(t,e);yield*i.entries(t,e)},validator:(t,e)=>ea(t,e).validator(t,e),coercer:(t,e)=>ea(t,e).coercer(t,e),refiner:(t,e)=>ea(t,e).refiner(t,e)}),s` + #sortable a:nth-of-type(2n) paper-icon-item { + animation-name: keyframes1; + animation-iteration-count: infinite; + transform-origin: 50% 10%; + animation-delay: -0.75s; + animation-duration: 0.25s; + } + + #sortable a:nth-of-type(2n-1) paper-icon-item { + animation-name: keyframes2; + animation-iteration-count: infinite; + animation-direction: alternate; + transform-origin: 30% 5%; + animation-delay: -0.5s; + animation-duration: 0.33s; + } + + #sortable a { + height: 48px; + display: flex; + } + + #sortable { + outline: none; + display: block !important; + } + + .hidden-panel { + display: flex !important; + } + + .sortable-fallback { + display: none; + } + + .sortable-ghost { + opacity: 0.4; + } + + .sortable-fallback { + opacity: 0; + } + + @keyframes keyframes1 { + 0% { + transform: rotate(-1deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(1.5deg); + animation-timing-function: ease-out; + } + } + + @keyframes keyframes2 { + 0% { + transform: rotate(1deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(-1.5deg); + animation-timing-function: ease-out; + } + } + + .show-panel, + .hide-panel { + display: none; + position: absolute; + top: 0; + right: 4px; + --mdc-icon-button-size: 40px; + } + + :host([rtl]) .show-panel { + right: initial; + left: 4px; + } + + .hide-panel { + top: 4px; + right: 8px; + } + + :host([rtl]) .hide-panel { + right: initial; + left: 8px; + } + + :host([expanded]) .hide-panel { + display: block; + } + + :host([expanded]) .show-panel { + display: inline-flex; + } + + paper-icon-item.hidden-panel, + paper-icon-item.hidden-panel span, + paper-icon-item.hidden-panel ha-icon[slot="item-icon"] { + color: var(--secondary-text-color); + cursor: pointer; + } +`;const ia=([[t,e],[i,o]],[n,a])=>[t*n+e*a,i*n+o*a],oa=([t,e],[i,o])=>[t+i,e+o],na=t=>t/180*Math.PI,aa=t=>{const{x:e,y:i,r:o,start:n,end:a,rotate:s=0}=t,r=e,l=i,c=o,d=o,h=na(n),u=(na(a)-h)%(2*Math.PI),m=na(s),p=(t=>[[Math.cos(t),-Math.sin(t)],[Math.sin(t),Math.cos(t)]])(m),[_,g]=oa(ia(p,[c*Math.cos(h),d*Math.sin(h)]),[r,l]),[f,b]=oa(ia(p,[c*Math.cos(h+u),d*Math.sin(h+u)]),[r,l]),v=u>Math.PI?1:0,y=u>0?1:0;return["M",_,g,"A",c,d,m/(2*Math.PI)*360,v,y,f,b].join(" ")};function sa(){return sa=Object.assign||function(t){for(var e=1;e-1}var Xa=function(){function t(t,e){this.manager=t,this.set(e)}var e=t.prototype;return e.set=function(t){t===ya&&(t=this.compute()),va&&this.manager.element.style&&Ma[t]&&(this.manager.element.style[ba]=t),this.actions=t.toLowerCase().trim()},e.update=function(){this.set(this.manager.options.touchAction)},e.compute=function(){var t=[];return Ya(this.manager.recognizers,(function(e){qa(e.options.enable,[e])&&(t=t.concat(e.getTouchAction()))})),function(t){if(Za(t,ka))return ka;var e=Za(t,Ta),i=Za(t,Sa);return e&&i?ka:e||i?e?Ta:Sa:Za(t,xa)?xa:wa}(t.join(" "))},e.preventDefaults=function(t){var e=t.srcEvent,i=t.offsetDirection;if(this.manager.session.prevented)e.preventDefault();else{var o=this.actions,n=Za(o,ka)&&!Ma[ka],a=Za(o,Sa)&&!Ma[Sa],s=Za(o,Ta)&&!Ma[Ta];if(n){var r=1===t.pointers.length,l=t.distance<2,c=t.deltaTime<250;if(r&&l&&c)return}if(!s||!a)return n||a&&i&Fa||s&&i&Wa?this.preventSrc(e):void 0}},e.preventSrc=function(t){this.manager.session.prevented=!0,t.preventDefault()},t}();function Ka(t,e){for(;t;){if(t===e)return!0;t=t.parentNode}return!1}function Ga(t){var e=t.length;if(1===e)return{x:pa(t[0].clientX),y:pa(t[0].clientY)};for(var i=0,o=0,n=0;n=_a(e)?t<0?Ha:Ra:e<0?Ia:Na}function is(t,e,i){return{x:e/t||0,y:i/t||0}}function os(t,e){var i=t.session,o=e.pointers,n=o.length;i.firstInput||(i.firstInput=Qa(e)),n>1&&!i.firstMultiple?i.firstMultiple=Qa(e):1===n&&(i.firstMultiple=!1);var a=i.firstInput,s=i.firstMultiple,r=s?s.center:a.center,l=e.center=Ga(o);e.timeStamp=ga(),e.deltaTime=e.timeStamp-a.timeStamp,e.angle=ts(r,l),e.distance=Ja(r,l),function(t,e){var i=e.center,o=t.offsetDelta||{},n=t.prevDelta||{},a=t.prevInput||{};e.eventType!==$a&&a.eventType!==za||(n=t.prevDelta={x:a.deltaX||0,y:a.deltaY||0},o=t.offsetDelta={x:i.x,y:i.y}),e.deltaX=n.x+(i.x-o.x),e.deltaY=n.y+(i.y-o.y)}(i,e),e.offsetDirection=es(e.deltaX,e.deltaY);var c,d,h=is(e.deltaTime,e.deltaX,e.deltaY);e.overallVelocityX=h.x,e.overallVelocityY=h.y,e.overallVelocity=_a(h.x)>_a(h.y)?h.x:h.y,e.scale=s?(c=s.pointers,Ja((d=o)[0],d[1],Ua)/Ja(c[0],c[1],Ua)):1,e.rotation=s?function(t,e){return ts(e[1],e[0],Ua)+ts(t[1],t[0],Ua)}(s.pointers,o):0,e.maxPointers=i.prevInput?e.pointers.length>i.prevInput.maxPointers?e.pointers.length:i.prevInput.maxPointers:e.pointers.length,function(t,e){var i,o,n,a,s=t.lastInterval||e,r=e.timeStamp-s.timeStamp;if(e.eventType!==Oa&&(r>La||void 0===s.velocity)){var l=e.deltaX-s.deltaX,c=e.deltaY-s.deltaY,d=is(r,l,c);o=d.x,n=d.y,i=_a(d.x)>_a(d.y)?d.x:d.y,a=es(l,c),t.lastInterval=e}else i=s.velocity,o=s.velocityX,n=s.velocityY,a=s.direction;e.velocity=i,e.velocityX=o,e.velocityY=n,e.direction=a}(i,e);var u,m=t.element,p=e.srcEvent;Ka(u=p.composedPath?p.composedPath()[0]:p.path?p.path[0]:p.target,m)&&(m=u),e.target=m}function ns(t,e,i){var o=i.pointers.length,n=i.changedPointers.length,a=e&$a&&o-n==0,s=e&(za|Oa)&&o-n==0;i.isFirst=!!a,i.isFinal=!!s,a&&(t.session={}),i.eventType=e,os(t,i),t.emit("hammer.input",i),t.recognize(i),t.session.prevInput=i}function as(t){return t.trim().split(/\s+/g)}function ss(t,e,i){Ya(as(e),(function(e){t.addEventListener(e,i,!1)}))}function rs(t,e,i){Ya(as(e),(function(e){t.removeEventListener(e,i,!1)}))}function ls(t){var e=t.ownerDocument||t;return e.defaultView||e.parentWindow||window}var cs=function(){function t(t,e){var i=this;this.manager=t,this.callback=e,this.element=t.element,this.target=t.options.inputTarget,this.domHandler=function(e){qa(t.options.enable,[t])&&i.handler(e)},this.init()}var e=t.prototype;return e.handler=function(){},e.init=function(){this.evEl&&ss(this.element,this.evEl,this.domHandler),this.evTarget&&ss(this.target,this.evTarget,this.domHandler),this.evWin&&ss(ls(this.element),this.evWin,this.domHandler)},e.destroy=function(){this.evEl&&rs(this.element,this.evEl,this.domHandler),this.evTarget&&rs(this.target,this.evTarget,this.domHandler),this.evWin&&rs(ls(this.element),this.evWin,this.domHandler)},t}();function ds(t,e,i){if(t.indexOf&&!i)return t.indexOf(e);for(var o=0;oi[e]})):o.sort()),o}var bs={touchstart:$a,touchmove:2,touchend:za,touchcancel:Oa},vs=function(t){function e(){var i;return e.prototype.evTarget="touchstart touchmove touchend touchcancel",(i=t.apply(this,arguments)||this).targetIds={},i}return ra(e,t),e.prototype.handler=function(t){var e=bs[t.type],i=ys.call(this,t,e);i&&this.callback(this.manager,e,{pointers:i[0],changedPointers:i[1],pointerType:Pa,srcEvent:t})},e}(cs);function ys(t,e){var i,o,n=gs(t.touches),a=this.targetIds;if(e&(2|$a)&&1===n.length)return a[n[0].identifier]=!0,[n,n];var s=gs(t.changedTouches),r=[],l=this.target;if(o=n.filter((function(t){return Ka(t.target,l)})),e===$a)for(i=0;i-1&&o.splice(t,1)}),ks)}}function Ss(t,e){t&$a?(this.primaryTouch=e.changedPointers[0].identifier,Ts.call(this,e)):t&(za|Oa)&&Ts.call(this,e)}function Ms(t){for(var e=t.srcEvent.clientX,i=t.srcEvent.clientY,o=0;o-1&&this.requireFail.splice(e,1),this},e.hasRequireFailures=function(){return this.requireFail.length>0},e.canRecognizeWith=function(t){return!!this.simultaneous[t.id]},e.emit=function(t){var e=this,i=this.state;function o(i){e.manager.emit(i,t)}i<8&&o(e.options.event+Ls(i)),o(e.options.event),t.additionalEvent&&o(t.additionalEvent),i>=8&&o(e.options.event+Ls(i))},e.tryEmit=function(t){if(this.canEmit())return this.emit(t);this.state=As},e.canEmit=function(){for(var t=0;te.threshold&&n&e.direction},i.attrTest=function(t){return Os.prototype.attrTest.call(this,t)&&(2&this.state||!(2&this.state)&&this.directionTest(t))},i.emit=function(e){this.pX=e.deltaX,this.pY=e.deltaY;var i=Vs(e.direction);i&&(e.additionalEvent=this.options.event+i),t.prototype.emit.call(this,e)},e}(Os),Rs=function(t){function e(e){return void 0===e&&(e={}),t.call(this,sa({event:"swipe",threshold:10,velocity:.3,direction:Fa|Wa,pointers:1},e))||this}ra(e,t);var i=e.prototype;return i.getTouchAction=function(){return Hs.prototype.getTouchAction.call(this)},i.attrTest=function(e){var i,o=this.options.direction;return o&(Fa|Wa)?i=e.overallVelocity:o&Fa?i=e.overallVelocityX:o&Wa&&(i=e.overallVelocityY),t.prototype.attrTest.call(this,e)&&o&e.offsetDirection&&e.distance>this.options.threshold&&e.maxPointers===this.options.pointers&&_a(i)>this.options.velocity&&e.eventType&za},i.emit=function(t){var e=Vs(t.offsetDirection);e&&this.manager.emit(this.options.event+e,t),this.manager.emit(this.options.event,t)},e}(Os),Is=function(t){function e(e){return void 0===e&&(e={}),t.call(this,sa({event:"pinch",threshold:0,pointers:2},e))||this}ra(e,t);var i=e.prototype;return i.getTouchAction=function(){return[ka]},i.attrTest=function(e){return t.prototype.attrTest.call(this,e)&&(Math.abs(e.scale-1)>this.options.threshold||2&this.state)},i.emit=function(e){if(1!==e.scale){var i=e.scale<1?"in":"out";e.additionalEvent=this.options.event+i}t.prototype.emit.call(this,e)},e}(Os),Ns=function(t){function e(e){return void 0===e&&(e={}),t.call(this,sa({event:"rotate",threshold:0,pointers:2},e))||this}ra(e,t);var i=e.prototype;return i.getTouchAction=function(){return[ka]},i.attrTest=function(e){return t.prototype.attrTest.call(this,e)&&(Math.abs(e.rotation)>this.options.threshold||2&this.state)},e}(Os),Fs=function(t){function e(e){var i;return void 0===e&&(e={}),(i=t.call(this,sa({event:"press",pointers:1,time:251,threshold:9},e))||this)._timer=null,i._input=null,i}ra(e,t);var i=e.prototype;return i.getTouchAction=function(){return[wa]},i.process=function(t){var e=this,i=this.options,o=t.pointers.length===i.pointers,n=t.distancei.time;if(this._input=t,!n||!o||t.eventType&(za|Oa)&&!a)this.reset();else if(t.eventType&$a)this.reset(),this._timer=setTimeout((function(){e.state=8,e.tryEmit()}),i.time);else if(t.eventType&za)return 8;return As},i.reset=function(){clearTimeout(this._timer)},i.emit=function(t){8===this.state&&(t&&t.eventType&za?this.manager.emit(this.options.event+"up",t):(this._input.timeStamp=ga(),this.manager.emit(this.options.event,this._input)))},e}($s),Ws={domEvents:!1,touchAction:ya,enable:!0,inputTarget:null,inputClass:null,cssProps:{userSelect:"none",touchSelect:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},Bs=[[Ns,{enable:!1}],[Is,{enable:!1},["rotate"]],[Rs,{direction:Fa}],[Hs,{direction:Fa},["swipe"]],[zs],[zs,{event:"doubletap",taps:2},["tap"]],[Fs]];function js(t,e){var i,o=t.element;o.style&&(Ya(t.options.cssProps,(function(n,a){i=fa(o.style,a),e?(t.oldCssProps[i]=o.style[i],o.style[i]=n):o.style[i]=t.oldCssProps[i]||""})),e||(t.oldCssProps={}))}var Us=function(){function t(t,e){var i,o=this;this.options=ha({},Ws,e||{}),this.options.inputTarget=this.options.inputTarget||t,this.handlers={},this.session={},this.recognizers=[],this.oldCssProps={},this.element=t,this.input=new((i=this).options.inputClass||(Ca?_s:Aa?vs:Da?Ds:xs))(i,ns),this.touchAction=new Xa(this,this.options.touchAction),js(this,!0),Ya(this.options.recognizers,(function(t){var e=o.add(new t[0](t[1]));t[2]&&e.recognizeWith(t[2]),t[3]&&e.requireFailure(t[3])}),this)}var e=t.prototype;return e.set=function(t){return ha(this.options,t),t.touchAction&&this.touchAction.update(),t.inputTarget&&(this.input.destroy(),this.input.target=t.inputTarget,this.input.init()),this},e.stop=function(t){this.session.stopped=t?2:1},e.recognize=function(t){var e=this.session;if(!e.stopped){var i;this.touchAction.preventDefaults(t);var o=this.recognizers,n=e.curRecognizer;(!n||n&&8&n.state)&&(e.curRecognizer=null,n=null);for(var a=0;a\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",n=window.console&&(window.console.warn||window.console.log);return n&&n.call(window.console,o,i),t.apply(this,arguments)}}var Ks=Xs((function(t,e,i){for(var o=Object.keys(e),n=0;n{const e=this._slider.getBoundingClientRect(),i=2*(t.center.x-e.left-e.width/2)/e.width,o=2*(t.center.y-e.top-e.height/2)/e.height,[,n]=function(t,e){return[Math.sqrt(t*t+e*e),Math.atan2(e,t)]}(i,o),a=(n/(2*Math.PI)*360+45-135+360)%360-45;return Math.max(Math.min(a/er,1),0)}}_valueToPercentage(t){return(Sn(t,this.min,this.max)-this.min)/(this.max-this.min)}_percentageToValue(t){return(this.max-this.min)*t+this.min}_steppedValue(t){return Math.round(t/this.step)*this.step}_boundedValue(t){var e,i;const o="high"===this._activeSlider?Math.min(null!==(e=this._localLow)&&void 0!==e?e:this.max):this.min,n="low"===this._activeSlider?Math.max(null!==(i=this._localHigh)&&void 0!==i?i:this.min):this.max;return Math.min(Math.max(t,o),n)}firstUpdated(t){super.firstUpdated(t),this._setupListeners()}updated(t){super.updated(t),this._activeSlider||(t.has("value")&&(this._localValue=this.value),t.has("low")&&(this._localLow=this.low),t.has("high")&&(this._localHigh=this.high))}connectedCallback(){super.connectedCallback(),this._setupListeners()}disconnectedCallback(){super.disconnectedCallback()}_findActiveSlider(t){var e,i;if(!this.dual)return"value";const o=Math.max(null!==(e=this._localLow)&&void 0!==e?e:this.min,this.min),n=Math.min(null!==(i=this._localHigh)&&void 0!==i?i:this.max,this.max);if(o>=t)return"low";if(n<=t)return"high";return Math.abs(t-o)<=Math.abs(t-n)?"low":"high"}_setActiveValue(t){switch(this._activeSlider){case"high":this._localHigh=t;break;case"low":this._localLow=t;break;case"value":this._localValue=t}}_getActiveValue(){switch(this._activeSlider){case"high":return this._localHigh;case"low":return this._localLow;case"value":return this._localValue}}_setupListeners(){this._interaction&&!this._mc&&(this._mc=new Us(this._interaction,{inputClass:Ds}),this._mc.add(new Hs({direction:Ba,enable:!0,threshold:0})),this._mc.add(new zs({event:"singletap"})),this._mc.on("pan",(t=>{t.srcEvent.stopPropagation(),t.srcEvent.preventDefault()})),this._mc.on("panstart",(t=>{var e,i;if(this.disabled)return;const o=this._getPercentageFromEvent(t),n=this._percentageToValue(o);this._activeSlider=this._findActiveSlider(n),this._lastSlider=this._activeSlider,null===(i=null===(e=this.shadowRoot)||void 0===e?void 0:e.getElementById("#slider"))||void 0===i||i.focus()})),this._mc.on("pancancel",(()=>{this.disabled||(this._activeSlider=void 0)})),this._mc.on("panmove",(t=>{if(this.disabled)return;const e=this._getPercentageFromEvent(t),i=this._percentageToValue(e),o=this._boundedValue(i);this._setActiveValue(o);const n=this._steppedValue(o);this._activeSlider&&Tn(this,`${this._activeSlider}-changing`,{value:n})})),this._mc.on("panend",(t=>{if(this.disabled)return;const e=this._getPercentageFromEvent(t),i=this._percentageToValue(e),o=this._boundedValue(i),n=this._steppedValue(o);this._setActiveValue(n),this._activeSlider&&(Tn(this,`${this._activeSlider}-changing`,{value:void 0}),Tn(this,`${this._activeSlider}-changed`,{value:n})),this._activeSlider=void 0})),this._mc.on("singletap",(t=>{var e,i;if(this.disabled)return;const o=this._getPercentageFromEvent(t),n=this._percentageToValue(o);this._activeSlider=this._findActiveSlider(n);const a=this._boundedValue(n),s=this._steppedValue(a);this._setActiveValue(s),this._activeSlider&&(Tn(this,`${this._activeSlider}-changing`,{value:void 0}),Tn(this,`${this._activeSlider}-changed`,{value:s})),this._lastSlider=this._activeSlider,null===(i=null===(e=this.shadowRoot)||void 0===e?void 0:e.getElementById("#slider"))||void 0===i||i.focus(),this._activeSlider=void 0})))}get _tenPercentStep(){return Math.max(this.step,(this.max-this.min)/10)}_handleKeyDown(t){var e,i,o;if(!ir.has(t.code))return;t.preventDefault(),this._lastSlider&&(null===(i=null===(e=this.shadowRoot)||void 0===e?void 0:e.getElementById(this._lastSlider))||void 0===i||i.focus()),this._activeSlider=null!==(o=this._lastSlider)&&void 0!==o?o:t.currentTarget.id,this._lastSlider=void 0;const n=this._getActiveValue();switch(t.code){case"ArrowRight":case"ArrowUp":this._setActiveValue(this._boundedValue((null!=n?n:this.min)+this.step));break;case"ArrowLeft":case"ArrowDown":this._setActiveValue(this._boundedValue((null!=n?n:this.min)-this.step));break;case"PageUp":this._setActiveValue(this._steppedValue(this._boundedValue((null!=n?n:this.min)+this._tenPercentStep)));break;case"PageDown":this._setActiveValue(this._steppedValue(this._boundedValue((null!=n?n:this.min)-this._tenPercentStep)));break;case"Home":this._setActiveValue(this._boundedValue(this.min));break;case"End":this._setActiveValue(this._boundedValue(this.max))}Tn(this,`${this._activeSlider}-changing`,{value:this._getActiveValue()}),this._activeSlider=void 0}_handleKeyUp(t){ir.has(t.code)&&(this._activeSlider=t.currentTarget.id,t.preventDefault(),Tn(this,`${this._activeSlider}-changing`,{value:void 0}),Tn(this,`${this._activeSlider}-changed`,{value:this._getActiveValue()}),this._activeSlider=void 0)}destroyListeners(){this._mc&&(this._mc.destroy(),this._mc=void 0)}_strokeCircleDashArc(t){return this._strokeDashArc(t,t)}_strokeDashArc(t,e){const i=this._valueToPercentage(t),o=this._valueToPercentage(e),n=290*Math.PI*er/360,a=Math.max((o-i)*n,0);return[`${a} ${n-a}`,`-${i*n-.5}`]}renderArc(t,e,i){var o,n;if(this.disabled)return Y;const a=aa({x:0,y:0,start:0,end:er,r:145}),s="end"===i?this.max:this.min,r=null!==(o=this.current)&&void 0!==o?o:s,l=null!=e?e:s,c="end"===i?l<=r:"start"===i&&r<=l,d=c?"end"===i?this._strokeDashArc(l,r):this._strokeDashArc(r,l):this._strokeCircleDashArc(l),h="full"===i?this._strokeDashArc(this.min,this.max):"end"===i?this._strokeDashArc(l,s):this._strokeDashArc(s,l),u=this._strokeCircleDashArc(l),m=null!=this.current&&this.current<=this.max&&this.current>=this.min&&(c||"full"===this.mode)?this._strokeCircleDashArc(this.current):void 0;return j` + + + + t??Y)(null!==(n=this.lowLabel)&&void 0!==n?n:this.label)} + @keydown=${this._handleKeyDown} + @keyup=${this._handleKeyUp} + /> + ${m?j` + + `:Y} + + + + `}render(){const t=aa({x:0,y:0,start:0,end:er,r:145}),e=this.dual?this._localLow:this._localValue,i=this._localHigh,o=this.current,n=o?this._strokeCircleDashArc(o):void 0;return B` + + + + + + + + ${n?j` + + `:Y} + ${null!=e?this.renderArc(this.dual?"low":"value",e,!this.dual&&this.mode||"start"):Y} + ${this.dual&&null!=i?this.renderArc("high",i,"end"):Y} + + + + + `}static get styles(){return s` + :host { + --clear-background-color: #111111; + --control-circular-slider-color: var(--primary-color); + --control-circular-slider-background: var(--disabled-color); + --control-circular-slider-background-opacity: 0.3; + --control-circular-slider-low-color: var( + --control-circular-slider-color + ); + --control-circular-slider-high-color: var( + --control-circular-slider-color + ); + } + #wrapper { + position: relative; + } + svg { + width: 320px; + display: block; + } + #slider { + width: 100%; + max-width: 233px; + outline: none; + margin: 0 auto; + margin-top: 1em; + position: relative; + } + #interaction { + display: flex; + fill: none; + stroke: transparent; + stroke-linecap: round; + stroke-width: 48px; + cursor: pointer; + } + #display { + pointer-events: none; + } + :host([disabled]) #interaction { + cursor: initial; + } + + .background { + fill: none; + stroke: var(--control-circular-slider-background); + opacity: var(--control-circular-slider-background-opacity); + transition: + stroke 180ms ease-in-out, + opacity 180ms ease-in-out; + stroke-linecap: round; + stroke-width: 24px; + } + + .arc { + fill: none; + stroke-linecap: round; + stroke-width: 24px; + transition: + stroke-width 300ms ease-in-out, + stroke-dasharray 300ms ease-in-out, + stroke-dashoffset 300ms ease-in-out, + stroke 180ms ease-in-out, + opacity 180ms ease-in-out; + } + + .target { + fill: none; + stroke-linecap: round; + stroke-width: 18px; + stroke: white; + transition: + stroke-width 300ms ease-in-out, + stroke-dasharray 300ms ease-in-out, + stroke-dashoffset 300ms ease-in-out, + stroke 180ms ease-in-out, + opacity 180ms ease-in-out; + } + + .target-border { + fill: none; + stroke-linecap: round; + stroke-width: 24px; + stroke: white; + transition: + stroke-width 300ms ease-in-out, + stroke-dasharray 300ms ease-in-out, + stroke-dashoffset 300ms ease-in-out, + stroke 180ms ease-in-out, + opacity 180ms ease-in-out; + } + + + + .current { + fill: none; + stroke-linecap: round; + stroke-width: 8px; + stroke: var(--primary-text-color); + opacity: 0.5; + transition: + stroke-width 300ms ease-in-out, + stroke-dasharray 300ms ease-in-out, + stroke-dashoffset 300ms ease-in-out, + stroke 180ms ease-in-out, + opacity 180ms ease-in-out; + } + + .arc-current { + stroke: var(--clear-background-color); + } + + .arc-clear { + stroke: var(--clear-background-color); + } + .arc-colored { + opacity: 0.5; + } + .arc-active { + outline: none; + } + .arc-active:focus-visible { + stroke-width: 28px; + } + + .pressed .arc, + .pressed .target, + .pressed .target-border, + .pressed .current { + transition: + stroke-width 300ms ease-in-out, + stroke 180ms ease-in-out, + opacity 180ms ease-in-out; + } + + .inactive .arc, + .inactive .arc-current { + opacity: 0; + } + + .value { + stroke: var(--control-circular-slider-color); + } + + .low { + stroke: var(--control-circular-slider-low-color); + } + + .high { + stroke: var(--state-climate-cool-color); + } + `}};function nr(t){var e,i;return B` + + ${t._showClassicPopup?function(t){var e,i,o;return B` +

+
+ t._closeClassicPopup()} + tabindex="0" + > + ${(null===(e=t._config)||void 0===e?void 0:e.disable_menu)?"":B` + {t._closeClassicPopup(),t._handleMoreInfo()}} + tabindex="0" + > + `} +
+ ${t._renderClassicContent(!0)} +
+
+ `}(t):""} + +
+
${t.name}
+
+
+ ${t._renderIcon(t.hvacMode,t.hvacMode,!0)} +
+
+
t._openClassicPopup()}> +
+ ${(()=>{var e,i,o,n;const a=(null===(e=t.hass)||void 0===e?void 0:e.locale)||void 0,s=new Intl.NumberFormat(a,{minimumFractionDigits:1,maximumFractionDigits:1}),r=t._display_top,l=t._display_bottom,c=(null===(n=null===(o=null===(i=t.hass)||void 0===i?void 0:i.config)||void 0===o?void 0:o.unit_system)||void 0===n?void 0:n.temperature)||"",d=null==r?t.hass?t.hass.localize("extra_states.temp_unavailable"):"":s.format(r),h=null==l?null:s.format(l);return B` +
+ ${d} ${c} + ${h?B`(${h} ${c})`:""} +
+ `})()} +
+
+
+
+
+ ${(null===(e=t._config)||void 0===e?void 0:e.disable_presets)?"":t.presets.map((e=>B`${t._renderPreset(e,t.preset)}`))} +
+
+
+ ${(null===(i=t._config)||void 0===i?void 0:i.allow_lock_toggle)?B` + + `:""} + ${function(t){var e,i,o,n;return(null===(e=t._config)||void 0===e?void 0:e.disable_timed_preset)?B``:B` +
+ ${t.timedPresetActive?B` + + ${t._formatRemainingTime(t.timedPresetRemainingTime)} + + ${(null!==(i=t.timedPresetRemainingTime)&&void 0!==i?i:0)<60?B` + ${_n({hass:t.hass,string:"extra_states.minutes"})} + `:""} + `:(null===(o=t._config)||void 0===o?void 0:o.use_manual_duration_input)?B` + + ${_n({hass:t.hass,string:"extra_states.minutes"})} + `:B` + + `} +
+ `}(t)} +
+
+
+ + `}t([ut({type:Boolean,reflect:!0})],or.prototype,"disabled",void 0),t([ut({type:Boolean})],or.prototype,"dual",void 0),t([ut({type:String})],or.prototype,"mode",void 0),t([ut({type:Boolean})],or.prototype,"inactive",void 0),t([ut({type:String})],or.prototype,"label",void 0),t([ut({type:String,attribute:"low-label"})],or.prototype,"lowLabel",void 0),t([ut({type:String,attribute:"high-label"})],or.prototype,"highLabel",void 0),t([ut({type:Number})],or.prototype,"value",void 0),t([ut({type:Number})],or.prototype,"low",void 0),t([ut({type:Number})],or.prototype,"high",void 0),t([ut({type:Number})],or.prototype,"current",void 0),t([ut({type:Number})],or.prototype,"step",void 0),t([ut({type:Number})],or.prototype,"min",void 0),t([ut({type:Number})],or.prototype,"max",void 0),t([mt()],or.prototype,"_localValue",void 0),t([mt()],or.prototype,"_localLow",void 0),t([mt()],or.prototype,"_localHigh",void 0),t([mt()],or.prototype,"_activeSlider",void 0),t([mt()],or.prototype,"_lastSlider",void 0),t([_t("#slider")],or.prototype,"_slider",void 0),t([_t("#interaction")],or.prototype,"_interaction",void 0),or=t([ct("vt-ha-control-circular-slider")],or);const ar=s` + :host([theme="gunmalmg"]) vt-ha-control-circular-slider { display: none; } + :host([theme="gunmalmg"]) vt-ha-control-circular-slider { display: none !important; } + :host([theme="gunmalmg"]) .disabled-circle-container { display: block; background: none; } + + /* Gunmalmg theme - minimalist compact list style */ + :host([theme="gunmalmg"]) ha-card { + color: var(--primary-text-color); + border-radius: 10px; + padding: 0px 0px; + box-shadow: none; + } + :host([theme="gunmalmg"]) .name { font-weight: 600; color: var(--primary-text-color); font-size: 17px; text-align: left;} + :host([theme="gunmalmg"]) .content { display: flex; position: relative; width: 100%; height: auto; max-width: none; transform: none; left: 0; top: 0; padding: 0; align-items: center; justify-content: center; } + :host([theme="gunmalmg"]) .current-info, :host([theme="gunmalmg"]) #left-infos, :host([theme="gunmalmg"]) #vt-control-buttons { display: none !important; } + :host([theme="gunmalmg"]) .disabled-circle-container { height: 84px; background: transparent; } + + /* Presets as pill buttons - single horizontal row */ + :host([theme="gunmalmg"]) #presets { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + margin-top: 0; + align-items: center; + gap: 6px; + margin-bottom: 0em; + width: max-content; + } + :host([theme="gunmalmg"]) .preset-label { + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0; padding: 0; + flex-shrink: 0; + height: 70px; + } + + :host([theme="gunmalmg"]) .preset-label ha-icon-button { + --mdc-icon-size: 50px; + width: 70px; + height: 70px; + border-radius: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--secondary-background-color); + color: var(--secondary-text-color); + border: 1px solid var(--divider-color); + } + :host([theme="gunmalmg"]) .preset-label .selected-icon { + box-shadow: 0 2px 0 rgba(0,0,0,0.3) inset; + } + + /* Presets base: icon grey, secondary background */ + :host([theme="gunmalmg"]) .preset-label ha-icon-button { + background: var(--secondary-background-color); + border: 1px solid var(--divider-color); + } + + /* Only selected preset icon should be colored */ + :host([theme="gunmalmg"]) .preset-label ha-icon-button.selected-icon { + background: var(--secondary-background-color); + color: white !important; + transition: background-color 180ms ease, color 180ms ease; + } + + /* Selected colors mapping */ + :host([theme="gunmalmg"]) .preset-frost ha-icon-button.selected-icon { background: #3a9ff2; } /* blue */ + :host([theme="gunmalmg"]) .preset-eco ha-icon-button.selected-icon { background: #5dd461; } /* green */ + :host([theme="gunmalmg"]) .preset-comfort ha-icon-button.selected-icon { background: #f9a21f; } /* orange */ + :host([theme="gunmalmg"]) .preset-boost ha-icon-button.selected-icon { background: #f75252; } /* red-ish */ + :host([theme="gunmalmg"]) .preset-activity ha-icon-button.selected-icon { background: #c455f7; } /* blue */ + /* Left-side compact hvac icon + action for gunmalmg */ + :host([theme="gunmalmg"]) #left-infos-gunmalmg { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + gap: 8px; + z-index: 4; + } + + :host([theme="gunmalmg"]) .hvac-mode ha-icon-button { + width: 56px; + height: 56px; + border-radius: 50%; + --mdc-icon-size: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--secondary-background-color); + color: var(--secondary-text-color); + border: none; + transition: background-color 220ms ease, color 220ms ease, transform 160ms ease; + } + + /* Colored states for hvac mode icon */ + :host([theme="gunmalmg"]) ha-card.heat .hvac-mode-tile ha-icon-button, + :host([theme="gunmalmg"]) ha-card.heat_cool .hvac-mode-tile ha-icon-button { + background: rgba(251, 150, 0, 0.15); + color: #fb9600; + } + :host([theme="gunmalmg"]) ha-card.cool .hvac-mode-tile ha-icon-button { + background: rgba(3, 141, 254, 0.15); + color: #038dfe; + } + :host([theme="gunmalmg"]) ha-card.off .hvac-mode-tile ha-icon-button { + background: var(--secondary-background-color); + color: var(--secondary-text-color); + } + + :host([theme="gunmalmg"]) .hvac-action svg { + width: 20px; + height: 20px; + fill: var(--primary-text-color); + opacity: 0.95; + } + + /* Badge: top-right white icon on colored background */ + :host([theme="gunmalmg"]) .hvac-badge-container { position: relative; display: inline-block; } + :host([theme="gunmalmg"]) .hvac-badge { + position: absolute; + top: -6px; + right: -6px; + width: 20px; + height: 20px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + } + :host([theme="gunmalmg"]) .hvac-badge.heating { background: #f44336; } + :host([theme="gunmalmg"]) .hvac-badge.cooling { background: #03A9F4; } + :host([theme="gunmalmg"]) .hvac-badge.warning { background: #ff9800; } + :host([theme="gunmalmg"]) .hvac-badge svg { fill: #ffffff; width: 14px; height: 14px; } /* badge always on colored bg */ + + /* Preset pills tweaks */ + :host([theme="gunmalmg"]) .preset-label { + border-radius: 10px; + display: inline-flex; + align-items: center; + gap: 8px; + } + + /* Temperatures look: main number larger, unit and secondary smaller */ + :host([theme="gunmalmg"]) .main-value { font-size: 16px; font-weight: 600; } + :host([theme="gunmalmg"]) #current { font-size: 12px; } + :host([theme="gunmalmg"]) .content .name + * { color: var(--secondary-text-color); font-size: 10px; } + + /* Inline temperature display (left-aligned, no wrapping) */ + :host([theme="gunmalmg"]) .gunmalmg-temps-inline { display: flex; gap: 4px; align-items: baseline; justify-content: flex-start; flex-wrap: nowrap; white-space: nowrap; } + :host([theme="gunmalmg"]) .gunmalmg-temp-main { font-size: 17px; font-weight: 700; color: var(--secondary-text-color); white-space: nowrap; } + :host([theme="gunmalmg"]) .gunmalmg-temp-main.has-error { color: var(--error-color, #db4437); } + :host([theme="gunmalmg"]) .gunmalmg-temp-main.has-error .gunmalmg-uom { color: var(--error-color, #db4437); } + :host([theme="gunmalmg"]) .gunmalmg-temp-secondary { font-size: 13px; color: var(--secondary-text-color); margin-left: 4px; white-space: nowrap; } + :host([theme="gunmalmg"]) .gunmalmg-uom { font-size: 11px; color: var(--secondary-text-color); margin-left: 1px; } + + /* Hide the classic timed-preset-container (we use our own gunmalmg-timed-preset) */ + :host([theme="gunmalmg"]) .timed-preset-container { display: none !important; } + + /* Grid layout: name spans columns 1-2 on first row; left/center/right on second row */ + :host([theme="gunmalmg"]) .gunmalmg-grid { + display: grid; + grid-template-columns: 0.8fr 1.6fr 2.4fr; + grid-template-areas: + "name name right" + "left center right"; + gap: 6px; + align-items: center; + } + + :host([theme="gunmalmg"]) .gunmalmg-name { + grid-area: name; + text-align: left; + padding-left: 8px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + line-height: 1.2; + margin: 0.5em; + } + :host([theme="gunmalmg"]) .gunmalmg-left { grid-area: left; } + :host([theme="gunmalmg"]) .gunmalmg-center { grid-area: center; } + :host([theme="gunmalmg"]) .gunmalmg-right { grid-area: right; } + + @media (max-width: 480px) { + :host([theme="gunmalmg"]) .gunmalmg-grid { + grid-template-columns: 1fr 1fr; + grid-template-areas: + "name right" + "left center"; + } + :host([theme="gunmalmg"]) .gunmalmg-right { + margin-top: 8px; + justify-content: center; + padding-right: 0; + } + } + + :host([theme="gunmalmg"]) .gunmalmg-left { background: transparent; padding: 0; display: flex; align-items: center; justify-content: center; } + :host([theme="gunmalmg"]) .gunmalmg-center { text-align: left; max-width: 100%; display:flex; flex-direction:column; justify-content:left; } + :host([theme="gunmalmg"]) .gunmalmg-right { + padding: 0; + display: flex; + flex-direction: row; + align-items: stretch; + min-width: 0; + } + + /* Scrollable presets zone */ + :host([theme="gunmalmg"]) .gunmalmg-presets-scroll { + flex: 1 1 0; + overflow-x: auto; + overflow-y: hidden; + min-width: 0; + scrollbar-width: thin; + scrollbar-color: var(--secondary-text-color, #888) rgba(128,128,128,0.15); + } + :host([theme="gunmalmg"]) .gunmalmg-presets-scroll::-webkit-scrollbar { height: 5px; } + :host([theme="gunmalmg"]) .gunmalmg-presets-scroll::-webkit-scrollbar-thumb { background: var(--secondary-text-color, #888); border-radius: 3px; } + :host([theme="gunmalmg"]) .gunmalmg-presets-scroll::-webkit-scrollbar-track { background: rgba(128,128,128,0.15); border-radius: 3px; } + + /* Fixed actions zone (lock + timed preset) */ + :host([theme="gunmalmg"]) .gunmalmg-actions { + flex: 0 0 auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + padding: 4px 4px; + gap: 4px; + min-width: 48px; + } + + :host([theme="gunmalmg"]) .hvac-mode-tile { display: flex; align-items: center; justify-content: center; padding: 0; background: transparent; } + + :host([theme="gunmalmg"]) .hvac-mode-tile ha-icon-button { overflow: hidden; box-shadow: none; border-radius: 30px; } + + /* Dim hvac mode and preset icons when locked */ + :host([theme="gunmalmg"]) ha-card.locked .hvac-mode-tile ha-icon-button, + :host([theme="gunmalmg"]) ha-card.locked #presets ha-icon-button, + :host([theme="gunmalmg"]) ha-card.locked #presets .preset-label, + :host([theme="gunmalmg"]) ha-card.locked .gunmalmg-timed-preset { + opacity: 0.45; + color: var(--disabled-text-color); + pointer-events: none; + } + + :host([theme="gunmalmg"]) .gunmalmg-presets { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + gap: 6px; + align-items: center; + justify-content: flex-start; + padding: 4px 4px; + min-height: 80px; + width: max-content; + } + + :host([theme="gunmalmg"]) .more-info { color: var(--primary-text-color); z-index: 6; } + + /* Lock button styles */ + :host([theme="gunmalmg"]) .gunmalmg-lock-btn { + --mdc-icon-size: 24px; + width: 40px; + height: 40px; + border-radius: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background-color 200ms ease, color 200ms ease; + } + :host([theme="gunmalmg"]) .gunmalmg-lock-btn.locked { + background: rgba(244, 67, 54, 0.20); + color: #f44336; + } + :host([theme="gunmalmg"]) .gunmalmg-lock-btn.unlocked { + background: rgba(76, 175, 80, 0.20); + color: #4CAF50; + } + + /* Timed preset styles for gunmalmg */ + :host([theme="gunmalmg"]) .gunmalmg-timed-preset { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + } + :host([theme="gunmalmg"]) .gunmalmg-timed-remaining { + font-size: 13px; + font-weight: 600; + color: #f9a21f; + white-space: nowrap; + cursor: pointer; + user-select: none; + border-radius: 8px; + padding: 2px 6px; + transition: background-color 200ms ease; + } + :host([theme="gunmalmg"]) .gunmalmg-timed-remaining:hover { + background: rgba(249, 162, 31, 0.15); + } + :host([theme="gunmalmg"]) .gunmalmg-timed-label { + font-size: 9px; + color: var(--secondary-text-color); + } + :host([theme="gunmalmg"]) .gunmalmg-timed-input { + width: 35px; + height: 25px; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--secondary-background-color); + color: var(--primary-text-color); + font-size: 12px; + text-align: center; + outline: none; + -moz-appearance: textfield; + } + :host([theme="gunmalmg"]) .gunmalmg-timed-input:focus { + border-color: #f9a21f; + } + :host([theme="gunmalmg"]) .gunmalmg-timed-input.active { + border-color: #f9a21f; + color: #f9a21f; + } + :host([theme="gunmalmg"]) .gunmalmg-timed-input::-webkit-outer-spin-button, + :host([theme="gunmalmg"]) .gunmalmg-timed-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + :host([theme="gunmalmg"]) .gunmalmg-timed-select { + width: 40px; + height: 30px; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--secondary-background-color); + color: var(--primary-text-color); + font-size: 10px; + text-align: center; + outline: none; + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: 0 4px; + } + :host([theme="gunmalmg"]) .gunmalmg-timed-select:focus { + border-color: #f9a21f; + } + :host([theme="gunmalmg"]) .gunmalmg-timed-select.active { + border-color: #f9a21f; + color: #f9a21f; + } + :host([theme="gunmalmg"]) .theme-menu { background: var(--card-background-color); color: var(--primary-text-color); border-color: var(--divider-color); } + :host([theme="gunmalmg"]) .theme-menu-item { color: var(--primary-text-color); } + :host([theme="gunmalmg"]) .theme-menu-header { display:flex; align-items:center; justify-content:space-between; padding: 8px 12px; border-bottom: 1px solid var(--divider-color); } + :host([theme="gunmalmg"]) .theme-menu-title { font-weight:600; cursor:pointer; } + + /* Gunmalmg center clickable area */ + :host([theme="gunmalmg"]) .gunmalmg-center { + cursor: pointer; + } + + /* Classic popup styles */ + :host([theme="gunmalmg"]) .classic-popup-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.7); + z-index: 1000; + } + + :host([theme="gunmalmg"]) .classic-popup-container { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1001; + background: var(--card-background-color, #fff); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + max-width: 95vw; + max-height: 90vh; + overflow: auto; + min-width: 300px; + } + + /* Mobile: full screen */ + @media (max-width: 600px) { + :host([theme="gunmalmg"]) .classic-popup-container { + width: 100vw; + height: 100vh; + max-width: 100vw; + max-height: 100vh; + border-radius: 0; + top: 0; + left: 0; + transform: none; + } + } + + /* Desktop: similar to more-info dialog */ + @media (min-width: 601px) { + :host([theme="gunmalmg"]) .classic-popup-container { + width: 450px; + max-height: 90vh; + } + } + + :host([theme="gunmalmg"]) .classic-popup-close { + position: absolute; + top: 8px; + left: 8px; + z-index: 1002; + --mdc-icon-size: 24px; + color: var(--primary-text-color); + } + + :host([theme="gunmalmg"]) .classic-popup-more-info { + position: absolute; + top: 8px; + right: 8px; + z-index: 1002; + --mdc-icon-size: 24px; + color: var(--secondary-text-color); + cursor: pointer; + border-radius: 100%; + } + + /* ===== Popup content: reset ALL gunmalmg overrides to classic appearance ===== */ + :host([theme="gunmalmg"]) .classic-popup-content { + position: relative; + padding: 1em; + padding-top: 1.5em; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + :host([theme="gunmalmg"]) .classic-popup-content.has-preset-mod { + padding-bottom: calc(1em + 38px); + } + + :host([theme="gunmalmg"]) .classic-popup-content.has-regulation-chart { + padding-bottom: calc(1em + 38px); + } + + :host([theme="gunmalmg"]) .classic-popup-content.has-preset-mod.has-regulation-chart { + padding-bottom: calc(1em + 76px); + } + + /* Same correction as in the main card: compensate for the 50% shift + caused by the popup content growing when panels are active. */ + :host([theme="gunmalmg"]) .classic-popup-content.has-preset-mod .content, + :host([theme="gunmalmg"]) .classic-popup-content.has-regulation-chart .content { + top: calc(50% - 40px - 19px) !important; + } + + :host([theme="gunmalmg"]) .classic-popup-content.has-preset-mod.has-regulation-chart .content { + top: calc(50% - 40px - 38px) !important; + } + + :host([theme="gunmalmg"]) .classic-popup-content.has-preset-mod .content.no-presets, + :host([theme="gunmalmg"]) .classic-popup-content.has-regulation-chart .content.no-presets { + top: calc(60% - 30px - 19px) !important; + } + + :host([theme="gunmalmg"]) .classic-popup-content.has-preset-mod.has-regulation-chart .content.no-presets { + top: calc(60% - 30px - 38px) !important; + } + + /* Name */ + :host([theme="gunmalmg"]) .classic-popup-content .name { + display: block; + width: 100%; + text-align: center; + font-size: 20px; + word-break: keep-all; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + color: var(--primary-text-color); + font-weight: normal; + } + + /* Circular slider: force visible */ + :host([theme="gunmalmg"]) .classic-popup-content vt-ha-control-circular-slider { + display: block !important; + } + + /* Content overlay inside slider: reset to classic absolute positioning */ + :host([theme="gunmalmg"]) .classic-popup-content .content { + position: absolute !important; + width: calc(70% - 40px) !important; + height: calc(70% - 100px) !important; + box-sizing: border-box; + border-radius: 100%; + left: 50% !important; + top: calc(50% - 40px) !important; + text-align: center; + overflow-wrap: break-word; + pointer-events: none; + display: flex !important; + align-items: center; + place-content: center; + flex-flow: wrap; + transform: translate(-50%,-50%) !important; + max-width: 155px !important; + padding: 0; + } + + :host([theme="gunmalmg"]) .classic-popup-content .content.no-presets { + top: calc(60% - 30px) !important; + } + + /* SVG inside popup */ + :host([theme="gunmalmg"]) .classic-popup-content #main { + transform: scale(2.3); + } + + :host([theme="gunmalmg"]) .classic-popup-content svg { + height: auto; + margin: auto; + display: block; + width: 100%; + -webkit-backface-visibility: hidden; + max-width: 233px; + } + + /* current-info: force visible */ + :host([theme="gunmalmg"]) .classic-popup-content .current-info { + display: flex !important; + } + + /* Reset font overrides to classic values */ + :host([theme="gunmalmg"]) .classic-popup-content .main-value { + font-size: inherit !important; + font-weight: normal !important; + } + :host([theme="gunmalmg"]) .classic-popup-content .main-value.temp-main { + font-size: 15px !important; + } + :host([theme="gunmalmg"]) .classic-popup-content .main-value.temp-secondary { + font-size: 6px !important; + } + + /* Hide gunmalmg badges in popup (classic has its own hvac action icon) */ + :host([theme="gunmalmg"]) .classic-popup-content .hvac-badge { + display: none !important; + } + + /* Temperature styles: reset to classic */ + :host([theme="gunmalmg"]) .classic-popup-content .temp-main { + font-size: 15px; + } + :host([theme="gunmalmg"]) .classic-popup-content .temp-main .uom { + font-size: 5px; + } + :host([theme="gunmalmg"]) .classic-popup-content .temp-secondary { + font-size: 6px; + } + :host([theme="gunmalmg"]) .classic-popup-content .temp-secondary .uom { + font-size: 3px; + } + + :host([theme="gunmalmg"]) .classic-popup-content line { + stroke: #e7e7e8; + stroke-width: 0.5; + } + + /* Modes: reset to classic */ + :host([theme="gunmalmg"]) .classic-popup-content #modes { + z-index: 0; + position: relative; + display: flex; + width: auto; + justify-content: center; + margin-top: -3em; + margin-bottom: 1em; + } + + /* Control buttons +/-: reset to classic absolute positioning */ + :host([theme="gunmalmg"]) .classic-popup-content #vt-control-buttons { + z-index: 0; + position: absolute !important; + display: block !important; + width: auto; + justify-content: center; + padding-bottom: 0.2em; + left: 90%; + top: 35%; + } + + /* Presets: reset to classic */ + :host([theme="gunmalmg"]) .classic-popup-content #presets { + z-index: 0; + position: relative; + display: flex; + width: 100%; + justify-content: center; + align-items: center; + margin-top: -1em; + margin-bottom: 1em; + flex-direction: row; + flex-wrap: wrap; + gap: 0; + } + + /* Preset labels: reset to classic style */ + :host([theme="gunmalmg"]) .classic-popup-content .preset-label { + cursor: pointer; + user-select: none; + display: flex; + position: relative; + align-items: center; + justify-content: flex-start; + overflow: hidden; + padding: 0 5px; + outline: 0px; + height: 48px; + margin: 0 5px; + border-radius: 0; + flex-shrink: initial; + } + + /* Preset icon buttons: reset to classic (no gunmalmg pill background) */ + :host([theme="gunmalmg"]) .classic-popup-content .preset-label ha-icon-button { + --mdc-icon-size: 24px; + width: auto; + height: auto; + border-radius: 100%; + background: transparent !important; + border: none !important; + box-shadow: none !important; + } + + /* Reset selected preset icon colors to classic */ + :host([theme="gunmalmg"]) .classic-popup-content .preset-label ha-icon-button.selected-icon { + background: transparent !important; + color: var(--label-badge-yellow) !important; + } + + /* Timed preset container: force visible */ + :host([theme="gunmalmg"]) .classic-popup-content .timed-preset-container { + display: flex !important; + } + + /* Left infos: reset to classic absolute positioning */ + :host([theme="gunmalmg"]) .classic-popup-content #left-infos { + z-index: 3; + position: absolute !important; + display: block !important; + width: auto; + justify-content: center; + padding-bottom: 0.2em; + left: 0%; + top: 15%; + } + + :host([theme="gunmalmg"]) .classic-popup-content #left-infos.no-presets { + top: 30%; + } + + /* Right lock: align center with +/- buttons */ + :host([theme="gunmalmg"]) .classic-popup-content #right-lock { + z-index: 0; + position: absolute; + left: 86%; + top: 15%; + display: flex; + align-items: center; + justify-content: center; + padding-right: 0; + } + + /* Disabled circle container fallback (when no slider) */ + :host([theme="gunmalmg"]) .classic-popup-content .disabled-circle-container { + height: 145px !important; + background: radial-gradient(var(--hvac-mode-color), transparent 50%) !important; + } + + :host([theme="gunmalmg"]) .classic-popup-content .disabled-circle-container.no-background-color { + background: none !important; + } +`,sr=s` + :host([theme="vtherm"]) ha-card { background: var(--card-background-color); color: var(--primary-text-color); } + :host([theme="vtherm"]) .vtherm-grid { display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 8px; align-items: center; } + :host([theme="vtherm"]) .vtherm-left { display:flex; justify-content:center; } + :host([theme="vtherm"]) .vtherm-center { text-align:center; } + :host([theme="vtherm"]) .vtherm-right { display:flex; justify-content:flex-end; } + :host([theme="vtherm"]) .more-info { z-index:6 } + :host([theme="vtherm"]) .theme-menu { background: var(--card-background-color); } + /* Dim hvac mode and preset icons when locked */ + :host([theme="vtherm"]) ha-card.locked .vtherm-left ha-icon-button, + :host([theme="vtherm"]) ha-card.locked .vtherm-right ha-icon-button, + :host([theme="vtherm"]) ha-card.locked .vtherm-right .preset-label { + opacity: 0.5; + color: var(--disabled-text-color); + pointer-events: none; + } +`;var rr;let lr=rr=class extends rt{constructor(){super(...arguments),this.hass=null,this.entityId="",this.visible=!1,this.targetTemp=null,this.roomTemp=null,this.regulatedTemp=null,this.extTemp=null,this.powerPercent=null,this._history=[],this._loading=!1,this._chart=null,this._canvas=null,this._chartReady=!1,this._destroyed=!1,this._lastEntityId="",this._initialViewSet=!1}connectedCallback(){super.connectedCallback(),this._destroyed=!1}disconnectedCallback(){super.disconnectedCallback(),this._destroyed=!0,this._destroyChart()}async firstUpdated(){var t;await this._initChart(),this._addLiveDataPoint(),this.entityId&&this.hass&&(this._lastEntityId=this.entityId,await this._fetchHistory()),this.visible&&this._chartReady&&(null===(t=this._chart)||void 0===t||t.resize(),this._updateChart(),this._applyInitialView())}updated(t){if(t.has("visible")&&this.visible&&this._chartReady)return void requestAnimationFrame((()=>{var t;null===(t=this._chart)||void 0===t||t.resize(),this._updateChart(),this._applyInitialView()}));if(t.has("entityId")&&this.entityId&&this.entityId!==this._lastEntityId)return this._lastEntityId=this.entityId,this._history=[],void this._fetchHistory().then((()=>{var t;this._chartReady&&this.visible&&(null===(t=this._chart)||void 0===t||t.resize(),this._updateChart(),this._initialViewSet=!1,this._applyInitialView())}));const e=["targetTemp","roomTemp","regulatedTemp","extTemp","powerPercent"].some((e=>t.has(e)));!e||null===this.targetTemp&&null===this.roomTemp&&null===this.powerPercent||(this._addLiveDataPoint(),this._chartReady&&this.visible&&this._updateChart())}async _fetchHistory(){if(!this.hass||!this.entityId)return;this._loading=!0;const t=new Date,e=`history/period/${new Date(t.getTime()-864e5).toISOString()}?filter_entity_id=${this.entityId}&end_time=${t.toISOString()}&significant_changes_only=false&minimal_response=false`;try{const t=await this.hass.callApi("GET",e);if(!Array.isArray(t)||0===t.length)return;const i=t[0].filter((t=>t.attributes&&t.last_updated)).map((t=>{var e,i,o,n,a,s,r,l,c,d,h,u,m,p,_,g,f,b,v,y,w,x,k;return{timestamp:new Date(t.last_updated).getTime(),targetTemp:null!==(e=t.attributes.temperature)&&void 0!==e?e:null,roomTemp:null!==(i=t.attributes.current_temperature)&&void 0!==i?i:null,regulatedTemp:null!==(l=null!==(s=null===(a=null===(n=null===(o=t.attributes)||void 0===o?void 0:o.vtherm_over_climate)||void 0===n?void 0:n.regulation)||void 0===a?void 0:a.regulated_target_temperature)&&void 0!==s?s:null===(r=t.attributes)||void 0===r?void 0:r.regulated_target_temperature)&&void 0!==l?l:null,extTemp:null!==(h=null===(d=null===(c=t.attributes)||void 0===c?void 0:c.specific_states)||void 0===d?void 0:d.ext_current_temperature)&&void 0!==h?h:null,powerPercent:null!==(k=null!==(v=null!==(g=null!==(m=null===(u=t.attributes)||void 0===u?void 0:u.power_percent)&&void 0!==m?m:null===(_=null===(p=t.attributes)||void 0===p?void 0:p.specific_states)||void 0===_?void 0:_.power_percent)&&void 0!==g?g:null===(b=null===(f=t.attributes)||void 0===f?void 0:f.vtherm_over_switch)||void 0===b?void 0:b.power_percent)&&void 0!==v?v:null===(x=null===(w=null===(y=t.attributes)||void 0===y?void 0:y.vtherm_over_climate)||void 0===w?void 0:w.valve_regulation)||void 0===x?void 0:x.power_percent)&&void 0!==k?k:null}})).filter((t=>null!==t.targetTemp||null!==t.roomTemp)),o=i.length>0?i[i.length-1].timestamp:0,n=this._history.filter((t=>t.timestamp>o));this._history=[...i,...n].slice(-rr.MAX_POINTS)}catch(t){console.error("[VtRegulationChart] History fetch failed:",t)}finally{this._loading=!1}}_addLiveDataPoint(){var t,e;const i=Date.now(),o=this._history[this._history.length-1],n={timestamp:i,targetTemp:this.targetTemp,roomTemp:this.roomTemp,regulatedTemp:null!==(t=this.regulatedTemp)&&void 0!==t?t:null,extTemp:null!==(e=this.extTemp)&&void 0!==e?e:null,powerPercent:this.powerPercent};o&&i-o.timestamp<3e4?this._history=[...this._history.slice(0,-1),n]:this._history=[...this._history,n].slice(-rr.MAX_POINTS)}_resolveColor(t,e){return getComputedStyle(this).getPropertyValue(t).trim()||e}async _initChart(){var t,e;if(!this._destroyed&&(this._canvas=null!==(e=null===(t=this.shadowRoot)||void 0===t?void 0:t.querySelector("canvas"))&&void 0!==e?e:null,this._canvas))try{const[{Chart:t,registerables:e},{default:i}]=await Promise.all([Promise.resolve().then((function(){return r_})),Promise.resolve().then((function(){return pg}))]);if(await Promise.resolve().then((function(){return Ob})),this._destroyed)return;t.register(...e,i);const o=this._resolveColor("--primary-text-color","#e5e5e5"),n=this._resolveColor("--secondary-text-color","#9e9e9e"),a="rgba(128,128,128,0.18)";this._chart=new t(this._canvas,{type:"line",data:{datasets:[{label:"Consigne",data:[],borderColor:"#f9a21f",backgroundColor:"rgba(249,162,31,0.08)",borderWidth:2,pointRadius:0,pointHoverRadius:4,tension:.3,yAxisID:"yTemp"},{label:"Pièce",data:[],borderColor:"#3a9ff2",backgroundColor:"rgba(58,159,242,0.08)",borderWidth:2,pointRadius:0,pointHoverRadius:4,tension:.3,yAxisID:"yTemp"},{label:"Régulée",data:[],borderColor:"#5dd461",backgroundColor:"rgba(93,212,97,0.08)",borderWidth:2,borderDash:[5,4],pointRadius:0,pointHoverRadius:4,tension:.3,yAxisID:"yTemp",hidden:!0},{label:"Ext.",data:[],borderColor:"#a78bfa",backgroundColor:"rgba(167,139,250,0.08)",borderWidth:1.5,borderDash:[3,3],pointRadius:1,pointHoverRadius:4,tension:.3,yAxisID:"yTemp",hidden:!0},{label:"Puissance %",data:[],borderColor:"#f75252",backgroundColor:"rgba(247,82,82,0.12)",borderWidth:1.5,pointRadius:1,pointHoverRadius:4,tension:.3,fill:!0,yAxisID:"yPercent",hidden:!0}]},options:{responsive:!0,maintainAspectRatio:!1,animation:!1,interaction:{mode:"index",intersect:!1},plugins:{legend:{display:!0,position:"top",labels:{boxWidth:12,font:{size:10},color:o,padding:8}},tooltip:{enabled:!0,callbacks:{label:t=>{const e=t.parsed.y,i="yPercent"===t.dataset.yAxisID?"%":"°";return null!==e?` ${t.dataset.label}: ${e.toFixed(1)}${i}`:""}}},zoom:{pan:{enabled:!0,mode:"x"},zoom:{wheel:{enabled:!0,speed:.015},pinch:{enabled:!0},mode:"x"},limits:{x:{minRange:6e4}}}},scales:{x:{type:"time",time:{tooltipFormat:"HH:mm:ss",displayFormats:{millisecond:"HH:mm:ss",second:"HH:mm:ss",minute:"HH:mm",hour:"HH:mm",day:"dd/MM"}},grid:{color:a},ticks:{maxTicksLimit:8,color:n,font:{size:10},maxRotation:0}},yTemp:{type:"linear",position:"left",grid:{color:a},ticks:{color:n,font:{size:10},callback:t=>`${t}°`}},yPercent:{type:"linear",position:"right",min:0,max:100,grid:{drawOnChartArea:!1},ticks:{color:"#f75252",font:{size:10},callback:t=>`${t}%`,stepSize:25}}}}}),this._chartReady=!0,this._updateChart()}catch(t){console.error("[VtRegulationChart] Failed to initialise Chart.js:",t)}}_updateChart(){if(!this._chart)return;const t=(t,e)=>t.filter((t=>null!==t[e]&&void 0!==t[e])).map((t=>({x:t.timestamp,y:t[e]}))),e=this._chart.data.datasets;e[0].data=t(this._history,"targetTemp"),e[1].data=t(this._history,"roomTemp"),e[2].data=t(this._history,"regulatedTemp"),e[3].data=t(this._history,"extTemp"),e[4].data=t(this._history,"powerPercent"),e[2].hidden=0===e[2].data.length,e[3].hidden=0===e[3].data.length,e[4].hidden=0===e[4].data.length,this._chart.update("none")}_applyInitialView(){!this._initialViewSet&&this._chart&&(this._initialViewSet=!0,this._zoomTo2h())}_zoomTo2h(){if(!this._chart)return;const t=Date.now();this._chart.zoomScale("x",{min:t-72e5,max:t},"none")}resetZoom(){var t,e;null===(e=null===(t=this._chart)||void 0===t?void 0:t.resetZoom)||void 0===e||e.call(t)}clearHistory(){this._history=[],this._updateChart()}_destroyChart(){this._chart&&(this._chart.destroy(),this._chart=null,this._chartReady=!1)}render(){return B` +
+ ${this._loading?B`
`:""} + +
+ + +
+
+ `}static get styles(){return s` + :host { + display: block; + width: 100%; + box-sizing: border-box; + } + + .chart-wrapper { + position: relative; + width: 100%; + height: 180px; + } + + canvas { + display: block; + width: 100% !important; + height: 100% !important; + } + + .zoom-btns { + position: absolute; + top: -4px; + right: 4px; + display: flex; + gap: 4px; + z-index: 10; + } + + .reset-zoom-btn { + position: static; + background: var(--secondary-background-color, rgba(0,0,0,0.08)); + border: 1px solid var(--divider-color, #ccc); + border-radius: 4px; + cursor: pointer; + font-size: 12px; + padding: 2px 6px; + color: var(--secondary-text-color, #666); + z-index: 10; + line-height: 1.4; + transition: background-color 150ms ease; + opacity: 0.7; + } + + .chart-loading { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 24px; + height: 24px; + border: 3px solid var(--divider-color, rgba(128,128,128,0.3)); + border-top-color: var(--primary-color, #03a9f4); + border-radius: 50%; + animation: vt-spin 0.8s linear infinite; + z-index: 5; + } + + @keyframes vt-spin { + to { transform: translate(-50%, -50%) rotate(360deg); } + } + `}};lr.MAX_POINTS=1440,t([ut({attribute:!1})],lr.prototype,"hass",void 0),t([ut({type:String})],lr.prototype,"entityId",void 0),t([ut({type:Boolean})],lr.prototype,"visible",void 0),t([ut({type:Number})],lr.prototype,"targetTemp",void 0),t([ut({type:Number})],lr.prototype,"roomTemp",void 0),t([ut({type:Number})],lr.prototype,"regulatedTemp",void 0),t([ut({type:Number})],lr.prototype,"extTemp",void 0),t([ut({type:Number})],lr.prototype,"powerPercent",void 0),t([mt()],lr.prototype,"_history",void 0),t([mt()],lr.prototype,"_loading",void 0),lr=rr=t([ct("vt-regulation-chart")],lr);const cr={auto:"M18,11V12.5C21.19,12.5 23.09,16.05 21.33,18.71L20.24,17.62C21.06,15.96 19.85,14 18,14V15.5L15.75,13.25L18,11M18,22V20.5C14.81,20.5 12.91,16.95 14.67,14.29L15.76,15.38C14.94,17.04 16.15,19 18,19V17.5L20.25,19.75L18,22M19,3H18V1H16V3H8V1H6V3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H14C13.36,20.45 12.86,19.77 12.5,19H5V8H19V10.59C19.71,10.7 20.39,10.94 21,11.31V5A2,2 0 0,0 19,3Z",heat_cool:"M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z",heat:"M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5H14.5Z",cool:"M20.79,13.95L18.46,14.57L16.46,13.44V10.56L18.46,9.43L20.79,10.05L21.31,8.12L19.54,7.65L20,5.88L18.07,5.36L17.45,7.69L15.45,8.82L13,7.38V5.12L14.71,3.41L13.29,2L12,3.29L10.71,2L9.29,3.41L11,5.12V7.38L8.5,8.82L6.5,7.69L5.92,5.36L4,5.88L4.47,7.65L2.7,8.12L3.22,10.05L5.55,9.43L7.55,10.56V13.45L5.55,14.58L3.22,13.96L2.7,15.89L4.47,16.36L4,18.12L5.93,18.64L6.55,16.31L8.55,15.18L11,16.62V18.88L9.29,20.59L10.71,22L12,20.71L13.29,22L14.7,20.59L13,18.88V16.62L15.5,15.17L17.5,16.3L18.12,18.63L20,18.12L19.53,16.35L21.3,15.88L20.79,13.95M9.5,10.56L12,9.11L14.5,10.56V13.44L12,14.89L9.5,13.44V10.56Z",off:"M16.56,5.44L15.11,6.89C16.84,7.94 18,9.83 18,12A6,6 0 0,1 12,18A6,6 0 0,1 6,12C6,9.83 7.16,7.94 8.88,6.88L7.44,5.44C5.36,6.88 4,9.28 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12C20,9.28 18.64,6.88 16.56,5.44M13,3H11V13H13",fan_only:St,dry:Vt,window_open:Ht,windowBypass:Rt,presence:Ct,motion:$t,eco:"M17,8C8,10 5.9,16.17 3.82,21.34L5.71,22L6.66,19.7C7.14,19.87 7.64,20 8,20C19,20 22,3 22,3C21,5 14,5.25 9,6.25C4,7.25 2,11.5 2,13.5C2,15.5 3.75,17.25 3.75,17.25C7,8 17,8 17,8Z",comfort:"M12.5 7C12.5 5.89 13.39 5 14.5 5H18C19.1 5 20 5.9 20 7V9.16C18.84 9.57 18 10.67 18 11.97V14H12.5V7M6 11.96V14H11.5V7C11.5 5.89 10.61 5 9.5 5H6C4.9 5 4 5.9 4 7V9.15C5.16 9.56 6 10.67 6 11.96M20.66 10.03C19.68 10.19 19 11.12 19 12.12V15H5V12C5 10.9 4.11 10 3 10S1 10.9 1 12V17C1 18.1 1.9 19 3 19V21H5V19H19V21H21V19C22.1 19 23 18.1 23 17V12C23 10.79 21.91 9.82 20.66 10.03Z",boost:"M13.13 22.19L11.5 18.36C13.07 17.78 14.54 17 15.9 16.09L13.13 22.19M5.64 12.5L1.81 10.87L7.91 8.1C7 9.46 6.22 10.93 5.64 12.5M21.61 2.39C21.61 2.39 16.66 .269 11 5.93C8.81 8.12 7.5 10.53 6.65 12.64C6.37 13.39 6.56 14.21 7.11 14.77L9.24 16.89C9.79 17.45 10.61 17.63 11.36 17.35C13.5 16.53 15.88 15.19 18.07 13C23.73 7.34 21.61 2.39 21.61 2.39M14.54 9.46C13.76 8.68 13.76 7.41 14.54 6.63S16.59 5.85 17.37 6.63C18.14 7.41 18.15 8.68 17.37 9.46C16.59 10.24 15.32 10.24 14.54 9.46M8.88 16.53L7.47 15.12L8.88 16.53M6.24 22L9.88 18.36C9.54 18.27 9.21 18.12 8.91 17.91L4.83 22H6.24M2 22H3.41L8.18 17.24L6.76 15.83L2 20.59V22M2 19.17L6.09 15.09C5.88 14.79 5.73 14.47 5.64 14.12L2 17.76V19.17Z",frost:"M14.46 9.41L11 7.38V5.12L12.71 3.41L11.29 2L10 3.29L8.71 2L7.29 3.41L9 5.12V7.38L6.5 8.82L4.5 7.69L3.92 5.36L2 5.88L2.47 7.65L.7 8.12L1.22 10.05L3.55 9.43L5.55 10.56V13.45L3.55 14.58L1.22 13.96L.7 15.89L2.47 16.36L2 18.12L3.93 18.64L4.55 16.31L6.55 15.18L9 16.62V18.88L7.29 20.59L8.71 22L10 20.71L11.29 22L12.7 20.59L11 18.88V16.62L14.46 14.61M7.5 10.56L10 9.11L12.5 10.56V13.44L10 14.89L7.5 13.44M19 5C17.89 5 17 5.89 17 7V13.76C16.36 14.33 16 15.15 16 16C16 17.66 17.34 19 19 19S22 17.66 22 16C22 15.15 21.64 14.33 21 13.77V7C21 5.89 20.11 5 19 5M19 6C19.55 6 20 6.45 20 7V8H18V7C18 6.45 18.45 6 19 6",activity:$t,power:"M12 3L2 12H5V20H19V12H22L12 3M11.5 18V14H9L12.5 7V11H15L11.5 18Z",flashAlert:Mt,temperature:"M15 13V5A3 3 0 0 0 9 5V13A5 5 0 1 0 15 13M12 4A1 1 0 0 1 13 5V8H11V5A1 1 0 0 1 12 4Z",humidity:Vt,ok:vt,thermometerAlert:Ot,none:"M23 17C23 20.31 20.31 23 17 23V21.5C19.5 21.5 21.5 19.5 21.5 17H23M1 7C1 3.69 3.69 1 7 1V2.5C4.5 2.5 2.5 4.5 2.5 7H1M8 4.32L3.41 8.92C.19 12.14 .19 17.37 3.41 20.59S11.86 23.81 15.08 20.59L22.15 13.5C22.64 13.03 22.64 12.24 22.15 11.75C21.66 11.26 20.87 11.26 20.38 11.75L15.96 16.17L15.25 15.46L21.79 8.92C22.28 8.43 22.28 7.64 21.79 7.15S20.5 6.66 20 7.15L14.19 13L13.5 12.27L20.37 5.38C20.86 4.89 20.86 4.1 20.37 3.61S19.09 3.12 18.6 3.61L11.71 10.5L11 9.8L16.5 4.32C17 3.83 17 3.04 16.5 2.55S15.22 2.06 14.73 2.55L7.11 10.17C8.33 11.74 8.22 14 6.78 15.45L6.07 14.74C7.24 13.57 7.24 11.67 6.07 10.5L5.72 10.15L9.79 6.08C10.28 5.59 10.28 4.8 9.79 4.31C9.29 3.83 8.5 3.83 8 4.32Z",auto_regulation_mode:Lt,power_percent:Lt,mean_power_cycle:"M7,2V13H10V22L17,10H13L17,2H7Z",valve_open_percent:"M22 13V21H20V19H16.58C15.81 20.76 14.05 22 12 22S8.19 20.76 7.42 19H4V21H2V13H4V15H7.43C7.93 13.85 8.85 12.93 10 12.42V11H8V9H16V11H14V12.42C15.15 12.93 16.07 13.85 16.57 15H20V13H22M17 2H7C6.45 2 6 2.45 6 3S6.45 4 7 4H10V5H11V8H13V5H14V4H17C17.55 4 18 3.55 18 3S17.55 2 17 2Z",regulated_target_temperature:"M12 14C11.7 13.6 11.4 13.3 11 13V5C11 3.3 9.7 2 8 2S5 3.3 5 5V13C2.8 14.7 2.3 17.8 4 20S8.8 22.7 11 21 13.7 16.2 12 14M9 8H7V5C7 4.5 7.5 4 8 4S9 4.5 9 5V8M18 3H16L12.8 12H14.7L15.4 10H18.6L19.3 12H21.2L18 3M15.8 8.7L17 5L18.2 8.7H15.8Z",auto_fan_mode:"M12.5 2C8.93 2 8.14 5.96 10.13 9.65C9.72 9.97 9.4 10.39 9.21 10.87C8.28 10.68 7.23 10.25 6.73 9.26C5.56 6.89 2 7 2 11.5C2 15.07 5.95 15.85 9.64 13.87C9.96 14.27 10.39 14.59 10.88 14.79C10.68 15.71 10.24 16.75 9.26 17.24C6.9 18.42 7 22 11.5 22C12.31 22 13 21.78 13.5 21.41C13.19 20.67 13 19.86 13 19C13 17.59 13.5 16.3 14.3 15.28C14.17 14.97 14.03 14.65 13.86 14.34C14.26 14 14.57 13.59 14.77 13.11C15.26 13.21 15.78 13.39 16.25 13.67C17.07 13.25 18 13 19 13C20.05 13 21.03 13.27 21.89 13.74C21.95 13.37 22 12.96 22 12.5C22 8.92 18.03 8.13 14.33 10.13C14 9.73 13.59 9.42 13.11 9.22C13.3 8.29 13.74 7.24 14.73 6.75C17.09 5.57 17 2 12.5 2M12 11C12.54 11 13 11.45 13 12C13 12.55 12.54 13 12 13C11.43 13 11 12.55 11 12C11 11.45 11.43 11 12 11M18 15C16.89 15 16 15.9 16 17V23H18V21H20V23H22V17C22 15.9 21.1 15 20 15M18 17H20V19H18Z",auto_fan_mode_off:"M12.5,2C9.64,2 8.57,4.55 9.29,7.47L15,13.16C15.87,13.37 16.81,13.81 17.28,14.73C18.46,17.1 22.03,17 22.03,12.5C22.03,8.92 18.05,8.13 14.35,10.13C14.03,9.73 13.61,9.42 13.13,9.22C13.32,8.29 13.76,7.24 14.75,6.75C17.11,5.57 17,2 12.5,2M3.28,4L2,5.27L4.47,7.73C3.22,7.74 2,8.87 2,11.5C2,15.07 5.96,15.85 9.65,13.87C9.97,14.27 10.4,14.59 10.89,14.79C10.69,15.71 10.25,16.75 9.27,17.24C6.91,18.42 7,22 11.5,22C13.8,22 14.94,20.36 14.94,18.21L18.73,22L20,20.72L3.28,4Z",fan_mode:St,power_sleep:zt,sleep:"M23,12H17V10L20.39,6H17V4H23V6L19.62,10H23V12M15,16H9V14L12.39,10H9V8H15V10L11.62,14H15V16M7,20H1V18L4.39,14H1V12H7V14L3.62,18H7V20Z"},dr="none",hr="off",ur="sleep",mr="auto_fan_none",pr="cooling",_r="heating",gr={auto_fan_none:"None",auto_fan_low:"Low",auto_fan_medium:"Medium",auto_fan_high:"High",auto_fan_turbo:"Turbo"},fr="classic",br="vtherm",vr="gunmalmg";function yr(t){const e=window;e.customCards=e.customCards||[],e.customCards.push(Object.assign(Object.assign({},t),{preview:!0}))}function wr(t){const e=(new Date).getTime()-t.getTime();return Math.floor(e/6e4)}function xr(t,e){const i=Math.round(t*e)/e;return parseFloat(i.toFixed(e))}console.info("%c VersatileThermostatUI-CARD \n%c version: 3.2.0 ","color: orange; font-weight: bold; background: black","color: white; font-weight: bold; background: dimgray"),yr({type:"versatile-thermostat-ui-card",name:"Versatile Thermostat Climate Card",description:"Card for climate entity"});let kr=class extends rt{_highChanged(t){if(this.isUserLocked)return;const e=t.detail.value;if(isNaN(e))return;const i=t.type.replace("-changed","");this.value=Object.assign(Object.assign({},this.value),{[i]:e}),this._selectTargetTemperature=i,this._callService(i)}_highChanging(t){if(this.isUserLocked)return;const e=t.detail.value;if(isNaN(e))return;const i=t.type.replace("-changing","");this.value=Object.assign(Object.assign({},this.value),{[i]:e}),this._selectTargetTemperature=i,this._updateDisplay(),this._vibrate(20)}_callService(t){this.isUserLocked||("high"!==t&&"low"!==t?this.hass.callService("climate","set_temperature",{entity_id:this.stateObj.entity_id,temperature:this.value.value}):this.hass.callService("climate","set_temperature",{entity_id:this.stateObj.entity_id,target_temp_low:this.value.low,target_temp_high:this.value.high}))}_handleButton(t){var e;if(this.isUserLocked)return;const i=t.currentTarget.target,o=t.currentTarget.step,n="high"===i?this.max:this.min;let a=null!==(e=this.value[i])&&void 0!==e?e:n;a+=o,a=Sn(a,this.min,this.max),"high"===i&&null!=this.value.low&&(a=Sn(a,this.value.low,this.max)),"low"===i&&null!=this.value.high&&(a=Sn(a,this.min,this.value.high)),this.value=Object.assign(Object.assign({},this.value),{[i]:a}),this._updateDisplay(),this._vibrate(40),this._debouncedCallService(i)}_handleTempUp(){var t;if(this.isUserLocked)return;const e=this.step||.5;let i="number"==typeof(null===(t=this.value)||void 0===t?void 0:t.value)?this.value.value:this.min;i+=e,i=Math.min(i,this.max),this.value=Object.assign(Object.assign({},this.value),{value:i}),this._callService("value")}_handleTempDown(){var t;if(this.isUserLocked)return;const e=this.step||.5;let i="number"==typeof(null===(t=this.value)||void 0===t?void 0:t.value)?this.value.value:this.min;i-=e,i=Math.max(i,this.min),this.value=Object.assign(Object.assign({},this.value),{value:i}),this._callService("value")}_handleSelectTemp(t){if(this.isUserLocked)return;const e=t.currentTarget.target;this._selectTargetTemperature=e,this._updateDisplay(),this._vibrate(40)}setConfig(t){this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t),this._gunmalmgPresetScrollDone=!1;const e=this._config&&this._config.theme?this._config.theme:"";this.theme=e,this.theme?this.setAttribute("theme",this.theme):this.setAttribute("theme","classic"),this._config&&this._config.theme&&this.setAttribute("theme",this._config.theme)}_applyThemeOverrides(t){}get effectiveDisableCircle(){return(this._config&&this._config.theme?this._config.theme:"classic")!==fr}get effectiveDisableBackgroundColor(){const t=this._config&&this._config.theme?this._config.theme:"classic";return t!==fr&&t!==br}getCardSize(){return 1}_vibrate(t){try{navigator.vibrate(t)}catch(t){}}_toggleThemeMenu(){this._showThemeMenu=!this._showThemeMenu}_closeThemeMenu(){this._showThemeMenu=!1}_openClassicPopup(){this._hasError&&(this.displayMessages=!0),this._showClassicPopup=!0}_closeClassicPopup(){this._showClassicPopup=!1,this.displayMessages=!1}_renderRegulationChartPanel(){var t,e,i,o,n,a,s,r,l,c,d,h,u,m;return(null===(t=this._config)||void 0===t?void 0:t.show_regulation_chart)?B` +
+
{this._regulationChartOpen=!this._regulationChartOpen}} + > + ${_n({hass:this.hass,string:"extra_states.regulation_chart_title"})} + +
+
+ +
+
+ `:B``}_renderPresetModificationPanel(){var t;if(!(null===(t=this._config)||void 0===t?void 0:t.allow_preset_modification))return B``;const e=["frost","eco","comfort","boost"],i={frost:_n({hass:this.hass,string:"extra_states.frost"}),eco:_n({hass:this.hass,string:"extra_states.eco"}),comfort:_n({hass:this.hass,string:"extra_states.comfort"}),boost:_n({hass:this.hass,string:"extra_states.boost"})},o=new Map;for(const t of this._presetTempEntities){const e=`${t.mode}_${t.away?"away":"present"}_${t.preset}`;o.set(e,t.entityId)}const n=[{mode:"heat",away:!1,labelKey:"extra_states.preset_row_heat"},{mode:"heat",away:!0,labelKey:"extra_states.preset_row_heat_away"},{mode:"cool",away:!1,labelKey:"extra_states.preset_row_cool"},{mode:"cool",away:!0,labelKey:"extra_states.preset_row_cool_away"}].filter((t=>e.some((e=>o.has(`${t.mode}_${t.away?"away":"present"}_${e}`))))),a=this._presetTempEntities.length>0,s=(t,e,i)=>{var n,a,s,r,l,c,d;const h=`${t}_${e?"away":"present"}_${i}`,u=o.get(h);if(!u)return B`–`;const m=null===(n=this.hass)||void 0===n?void 0:n.states[u],p=m?parseFloat(m.state):null,_=null!==(s=null===(a=null==m?void 0:m.attributes)||void 0===a?void 0:a.step)&&void 0!==s?s:.5,g=null!==(l=null===(r=null==m?void 0:m.attributes)||void 0===r?void 0:r.min)&&void 0!==l?l:7,f=null!==(d=null===(c=null==m?void 0:m.attributes)||void 0===c?void 0:c.max)&&void 0!==d?d:35,b=t=>{const e=Math.round(Math.min(f,Math.max(g,t))/_)*_,i=Math.round(100*e)/100;this.hass.callService("number","set_value",{entity_id:u,value:i})};return B` + +
+ {const e=parseFloat(t.target.value);isNaN(e)||b(e)}} + /> +
+ + +
+
+ + `};return B` +
+
{this._isLocked||(this._presetsPanelOpen=!this._presetsPanelOpen)}} + title=${_n({hass:this.hass,string:"extra_states.preset_mod_title"})} + > + ${_n({hass:this.hass,string:"extra_states.preset_mod_title"})} + +
+ ${this._presetsPanelOpen?B` +
+ ${a?B` + + + + + ${e.map((t=>B``))} + + + + ${n.map((t=>B` + + + ${e.map((e=>s(t.mode,t.away,e)))} + + `))} + +
${i[t]}
+ ${_n({hass:this.hass,string:t.labelKey})} +
+ `:B` +
+ ${_n({hass:this.hass,string:"extra_states.preset_mod_no_entities"})} +
+ `} +
+ `:""} +
+ `}_renderClassicContent(t=!1){var e,i,o,n,a,s,r,l,c,d,h,u,m,p,_,g,f,b,v,y,w,x,k;const T=!t&&this.effectiveDisableCircle,S=!t&&this.effectiveDisableBackgroundColor;this._renderingAsClassic=t;const M=B` + ${this.name.length>0?B` +
${this.name}
+ `:""} + + ${(null===(e=this.safety_state)||void 0===e?void 0:e.length)>0&&!this.displayMessages?B` +
+ + + ${B` + ${this.safety_state.map((t=>B`${t.name}: ${t.security_msg}`))} + `} +
+ `:""} + ${this.messages.length>0&&this.displayMessages?B` +
+ + + ${this.messages.map((t=>B`${t}`))} +
+ `:""} + +
+ ${T?B` + + `:null!=this.value.low&&null!=this.value.high&&"unavailable"!==(null===(i=this.stateObj)||void 0===i?void 0:i.state)?B` + + `:B` + + `} +
+ + + ${this._hasWindowByPass?j` + + + + + `:""} + ${this._hasWindowByPass||!this._hasWindow||(null===(s=this._config)||void 0===s?void 0:s.disable_window)?"":j` + + + + + `} + ${this._hasOverpowering&&!(null===(l=this._config)||void 0===l?void 0:l.disable_overpowering)?j` + + `:""} + ${this._hasPresence?j` + + `:""} + ${this._hasAutoStartStop&&!(null===(d=this._config)||void 0===d?void 0:d.disable_autoStartStop)?j` + + `:""} + ${this._hasMotion?j` + + `:""} + + + ${T?j` + ${this._renderStatusText("62.5","50")} + ${this._renderTemperature(this._display_top,!0,"55","62%",!(null===(u=null==this?void 0:this._config)||void 0===u?void 0:u.set_current_as_main))} + ${this._renderTemperature(this._display_bottom,!1,"90","62%",1==(null===(m=null==this?void 0:this._config)||void 0===m?void 0:m.set_current_as_main))} + + ${this._renderHVACAction()} + + `:j` + ${this._renderStatusText("62.5","50")} + ${this._renderTemperature(this._display_top,!0,"50%","62%",!(null===(p=null==this?void 0:this._config)||void 0===p?void 0:p.set_current_as_main))} + + + ${this._renderTemperature(this._display_bottom,!1,"-5%","0%",1==(null===(_=null==this?void 0:this._config)||void 0===_?void 0:_.set_current_as_main))} + ${this._renderHVACAction()} + + `} + +
+ ${T?"":B` +
`} +
+
+ ${j` + ${this.modes.map((t=>{var e,i,o,n,a,s,r,l;return(null===(e=this._config)||void 0===e?void 0:e.disable_heat)&&"heat"===t||(null===(i=this._config)||void 0===i?void 0:i.disable_auto)&&"auto"===t||(null===(o=this._config)||void 0===o?void 0:o.disable_cool)&&"cool"===t||(null===(n=this._config)||void 0===n?void 0:n.disable_heat_cool)&&"heat_cool"===t||(null===(a=this._config)||void 0===a?void 0:a.disable_dry)&&"dry"===t||(null===(s=this._config)||void 0===s?void 0:s.disable_fan_only)&&"fan_only"===t||(null===(r=this._config)||void 0===r?void 0:r.disable_off)&&"off"===t||(null===(l=this._config)||void 0===l?void 0:l.disable_sleep)&&"sleep"===t?B``:this._renderIcon(t,this.hvacMode)}))} + `} +
+ ${(null===(g=null==this?void 0:this._config)||void 0===g?void 0:g.disable_buttons)?B``:B` +
+
+ + + +
+
+ + + +
+
+ `} + + ${(null===(f=this._config)||void 0===f?void 0:f.disable_presets)?"":B` +
+ ${j` + ${this.presets.map((t=>this._renderPreset(t,this.preset)))} + `} + ${(null===(b=this._config)||void 0===b?void 0:b.disable_timed_preset)?"":B` +
+ ${this.timedPresetActive?B` + + ${this._formatRemainingTime(this.timedPresetRemainingTime)} + + ${(null!==(v=this.timedPresetRemainingTime)&&void 0!==v?v:0)<60?B` + ${_n({hass:this.hass,string:"extra_states.minutes"})} + `:""} + `:(null===(y=this._config)||void 0===y?void 0:y.use_manual_duration_input)?B` + + ${_n({hass:this.hass,string:"extra_states.minutes"})} + `:B` + + `} +
+ `} +
+ `} + +
+ ${this.isRecalculateScheduled?j` + ${this._renderRecalculateScheduledButton()} + `:""} + ${this.messages.length>0?j` + ${this._renderMessagesButton()} + `:""} + ${this._config.autoStartStopEnableEntity&&this._isAutoStartStopConfigured?j` + ${this._renderAutoStartStopEnable()} + `:""} + ${j` + ${this._externalPowerInfos.map((t=>this._renderPowerInfo(t)))} + `} + ${j` + ${this.powerInfos.map((t=>this._renderPowerInfo(t)))} + `} + ${j` + ${this.autoFanInfos.map((t=>this._renderAutoFanInfo(t)))} + `} +
+ +
+ ${(null===(k=this._config)||void 0===k?void 0:k.allow_lock_toggle)?B` + + `:""} +
+
+ ${this._renderPresetModificationPanel()} + ${this._renderRegulationChartPanel()} +
+ `;return this._renderingAsClassic=!1,M}_applyTheme(t){this._config||(this._config={}),this._config.theme=t,Tn(this,"config-changed",{config:this._config}),this._closeThemeMenu(),this.setAttribute("theme",t),this._applyThemeOverrides(t)}async _menuLockToggle(){var t;if((null===(t=this._config)||void 0===t?void 0:t.entity)&&this.hass&&this.stateObj){if(this._hasLockCode)return this.isLocking=!this._isLocked,this.showDigicodeModal=!0,void(this.enteredCode="");try{this._isLocked?this._isLockConfigured?await this.hass.callService("versatile_thermostat","unlock",{entity_id:this._config.entity}):(this._isLocked=this.isUserLocked=!1,this.requestUpdate()):this._isLockConfigured?await this.hass.callService("versatile_thermostat","lock",{entity_id:this._config.entity}):(this._isLocked=this.isUserLocked=!0,this.requestUpdate())}catch(t){console.error(t)}}}_menuCancelTimedPreset(){this.timedPresetActive&&this._handleCancelTimedPreset()}firstUpdated(t){this._init=!1}shouldUpdate(t){return void 0!==t.has("_config")&&void 0!==t.get("_config")&&(this._hasOverpowering=!1,this._hasWindow=!1,this._hasWindowByPass=!1,this._hasMotion=!1,this._hasPresence=!1,this._hasAutoStartStop=!1,this._isAutoStartStopEnabled=!1,this.humidity=0),void 0!==t.get("hass")&&(this._init=!1),!0}updated(t){var e,i,o;super.updated(t),this._firstRender=!1,"gunmalmg"!==(null===(e=this._config)||void 0===e?void 0:e.theme)||this._gunmalmgPresetScrollDone||requestAnimationFrame((()=>{var t;const e=null===(t=this.shadowRoot)||void 0===t?void 0:t.querySelector(".gunmalmg-presets-scroll"),i=null==e?void 0:e.querySelector(".selected-icon");if(i&&e){this._gunmalmgPresetScrollDone=!0;const t=i.closest(".preset-label")||i,o=t.offsetLeft,n=t.offsetWidth,a=e.clientWidth,s=n,r=Math.max(0,o-(a-n)/2+s);e.scrollLeft=r}})),t.has("showDigicodeModal")&&(this.showDigicodeModal?window.addEventListener("keydown",this._handleKeyDown):window.removeEventListener("keydown",this._handleKeyDown)),null===(o=null===(i=null==this?void 0:this.shadowRoot)||void 0===i?void 0:i.querySelector(".security"))||void 0===o||o.addEventListener("click",(()=>{var t,e,i,o;null===(e=null===(t=null==this?void 0:this.shadowRoot)||void 0===t?void 0:t.querySelector(".security"))||void 0===e||e.remove(),null===(o=null===(i=null==this?void 0:this.shadowRoot)||void 0===i?void 0:i.querySelector(".content"))||void 0===o||o.classList.remove("security_msg"),this._vibrate(2)}))}_willUpdatePower(){let t,e,i;this._externalPowerInfos=[],this._config.powerEntity&&(console.log(`There is a powerEntity ${this._config.powerEntity}`),t=this.hass.states[this._config.powerEntity],e=Number(t.state),i=t.attributes.unit_of_measurement,console.log(`Power found ${e} ${i} for ${name}`)),e&&(console.log("J'ai pushé du power"),this._externalPowerInfos.push({name:"mean_power_cycle",value:e,unit:i,class:"vt-power-color"}))}_classifyPresetEntity(t){const e=t.toLowerCase(),i=/_cool_/.test(e)||e.endsWith("_cool")||/_ac_(away_)?temp$/.test(e),o=e.includes("_away");let n=null;return e.includes("frost")||e.includes("hors_gel")?n="frost":e.includes("eco")?n="eco":e.includes("comfort")?n="comfort":e.includes("boost")&&(n="boost"),n?{entityId:t,preset:n,mode:i?"cool":"heat",away:o}:null}_discoverPresetTempEntities(){var t,e;if(!this.hass||!(null===(t=this._config)||void 0===t?void 0:t.entity))return;const i=this.hass.entities;if(!i)return void console.log("[PresetMod] hass.entities not available");const o=i[this._config.entity];if(!(null==o?void 0:o.device_id))return void console.log(`[PresetMod] No device_id for entity ${this._config.entity}`);const n=o.device_id;console.log(`[PresetMod] device_id=${n}`);const a=[];for(const[t,o]of Object.entries(i)){if(o.device_id!==n)continue;if(!t.startsWith("number."))continue;const i=this.hass.states[t];if(!i)continue;if("temperature"!==(null===(e=i.attributes)||void 0===e?void 0:e.device_class))continue;const s=this._classifyPresetEntity(t);s&&(console.log(`[PresetMod] found entity: ${t} preset=${s.preset} mode=${s.mode} away=${s.away}`),a.push(s))}const s=this._presetTempEntities.map((t=>t.entityId)).join(","),r=a.map((t=>t.entityId)).join(",");s!==r&&(this._presetTempEntities=a)}willUpdate(t){var e,i,o,n,a,s,r,l,c,d,h,u,m,p,_,g,f,b,v,y,w,x,k,T,S,M,D,C,A,P,E,L,$,z,O,V,H,R,I,N,F,W,B,j,U,Y,q,Z,X,K,G,Q,J;if(!this.hass||!this._config||!t.has("hass")&&!t.has("_config"))return;const tt=this._config.entity;this._willUpdatePower(),(null===(e=this._config)||void 0===e?void 0:e.allow_preset_modification)&&this._discoverPresetTempEntities();const et=this.hass.states[tt];if(!et)return void console.log("No state");const it=t.get("hass");if(!it||it.states[tt]!==et){if(!this._config||!this.hass||!this._config.entity)return void console.log("No change return");console.log("Something may have change"),this.stateObj=et;const t=this.stateObj.attributes,e=this.stateObj.state;this._isLockConfigured=!0===(null==t?void 0:t.is_lock_configured),(null==t?void 0:t.lock_manager)&&(this._isLocked=(null===(i=null==t?void 0:t.lock_manager)||void 0===i?void 0:i.is_locked)||!1),this.isUserLocked=this._isLocked&&(!this._isLockConfigured||(null===(o=null==t?void 0:t.lock_manager)||void 0===o?void 0:o.lock_users)||!1),this.isAutomationLocked=this._isLocked&&((null===(n=null==t?void 0:t.lock_manager)||void 0===n?void 0:n.lock_automations)||!1),this._hasLockCode=!0===(null===(a=null==t?void 0:t.lock_manager)||void 0===a?void 0:a.lock_code),console.log(`Lock states. isConfigured:${this._isLockConfigured} isLocked=${this._isLocked} isUserLocked=${this.isUserLocked} isAutomationLocked=${this.isAutomationLocked} hasLockCode=${this._hasLockCode}`),this.name="",this.hvacMode=e||hr,this.hvacAction=null==t?void 0:t.hvac_action,this.preset=null==t?void 0:t.preset_mode,this.modes=(null==t?void 0:t.hvac_modes)?Object.values(t.hvac_modes):[],this.presets=t.preset_modes?Object.values(t.preset_modes):[],this.isSleeping=!0===(null===(s=null==t?void 0:t.specific_states)||void 0===s?void 0:s.is_sleeping),this.powerPercent=(null===(r=null==t?void 0:t.vtherm_over_switch)||void 0===r?void 0:r.power_percent)||(null===(c=null===(l=null==t?void 0:t.vtherm_over_climate)||void 0===l?void 0:l.valve_regulation)||void 0===c?void 0:c.power_percent)||0,void 0===(null===(d=null==t?void 0:t.specific_states)||void 0===d?void 0:d.is_device_active)?(this.isDeviceActive=this.hvacAction==_r||this.hvacAction==pr,console.log(`is_device_active not found value=${this.isDeviceActive}`)):(this.isDeviceActive=!0===(null===(h=null==t?void 0:t.specific_states)||void 0===h?void 0:h.is_device_active),console.log(`is_device_active found value=${this.isDeviceActive}`)),this.temperature=null==t?void 0:t.temperature,this.step=(null===(u=null==t?void 0:t.configuration)||void 0===u?void 0:u.target_temperature_step)||.5,this.min=(null==t?void 0:t.min_temp)||7,this.max=(null==t?void 0:t.max_temp)||35,this.current=(null==t?void 0:t.current_temperature)||0,this.windowState=null===(m=null==t?void 0:t.window_manager)||void 0===m?void 0:m.window_state,this.windowAutoState=null===(p=null==t?void 0:t.window_manager)||void 0===p?void 0:p.window_auto_state,this.humidity=(null==t?void 0:t.humidity)?parseFloat(t.humidity):0,this.overpoweringState=(null===(_=null==t?void 0:t.power_manager)||void 0===_?void 0:_.overpowering_state)||"off",this.presenceState=(null===(g=null==t?void 0:t.presence_manager)||void 0===g?void 0:g.presence_state)||"off",this.motionState=(null===(f=null==t?void 0:t.motion_manager)||void 0===f?void 0:f.motion_state)||"off",this._hasWindowByPass=!0===(null===(b=null==t?void 0:t.window_manager)||void 0===b?void 0:b.is_window_bypass),this.safetyState=(null===(v=null==t?void 0:t.safety_manager)||void 0===v?void 0:v.safety_state)||"off",this.meanCyclePower=(null===(y=null==t?void 0:t.power_manager)||void 0===y?void 0:y.mean_cycle_power)||0,this.valveOpenPercent=(null===(w=null==t?void 0:t.vtherm_over_valve)||void 0===w?void 0:w.valve_open_percent)||(null===(k=null===(x=null==t?void 0:t.vtherm_over_climate_valve)||void 0===x?void 0:x.valve_regulation)||void 0===k?void 0:k.valve_open_percent)||0,this.devicePower=(null===(T=null==t?void 0:t.power_manager)||void 0===T?void 0:T.device_power)||0,this.isRegulated=!0===(null===(S=null==t?void 0:t.vtherm_over_climate)||void 0===S?void 0:S.is_regulated),this.regulatedTargetTemperature=(null===(D=null===(M=null==t?void 0:t.vtherm_over_climate)||void 0===M?void 0:M.regulation)||void 0===D?void 0:D.regulated_target_temperature)||null,this.autoRegulationMode=(null===(A=null===(C=null==t?void 0:t.vtherm_over_climate)||void 0===C?void 0:C.regulation)||void 0===A?void 0:A.auto_regulation_mode)||null,this.currentAutoFanMode=(null===(P=null==t?void 0:t.vtherm_over_climate)||void 0===P?void 0:P.current_auto_fan_mode)||null,this.autoFanMode=(null===(E=null==t?void 0:t.vtherm_over_climate)||void 0===E?void 0:E.auto_fan_mode)||null,this.fanMode=(null==t?void 0:t.fan_mode)||null,this.hvacOffReason=(null===(L=null==t?void 0:t.specific_states)||void 0===L?void 0:L.hvac_off_reason)||null,this.isRecalculateScheduled=(null===($=null==t?void 0:t.specific_states)||void 0===$?void 0:$.is_recalculate_scheduled)||null,this.isOn=!0===(null===(z=null==t?void 0:t.specific_states)||void 0===z?void 0:z.is_on),this.timedPresetActive=!0===(null===(O=null==t?void 0:t.timed_preset_manager)||void 0===O?void 0:O.is_active),this.timedPresetRemainingTime=(null===(V=null==t?void 0:t.timed_preset_manager)||void 0===V?void 0:V.remaining_time_min)||null,this.timedPresetPreset=(null===(H=null==t?void 0:t.timed_preset_manager)||void 0===H?void 0:H.preset)||null;const tt=(null===(R=null==t?void 0:t.requested_state)||void 0===R?void 0:R.hvac_mode)||null;let it=(null===(I=null==t?void 0:t.specific_states)||void 0===I?void 0:I.messages)||[];it&&!Array.isArray(it)&&(it=[it]);const ot=1==(null===(N=null==t?void 0:t.vtherm_over_climate_valve)||void 0===N?void 0:N.have_valve_regulation);if((null===(F=this._config)||void 0===F?void 0:F.disable_name)||(this.name=this._config.name?this._config.name:t.friendly_name),this.hvacMode==hr&&!0===this.isSleeping&&(this.hvacMode=ur),console.log(`Mode is ${this.hvacMode}`),!0===(null==t?void 0:t.is_over_switch)&&this.powerPercent>0&&(this.hvacAction="heat"==tt?_r:pr,console.log(`After hvac_action patch ${this.hvacAction}`)),console.log(`Modes are ${this.modes}`),this.modes.length>1&&this.modes.includes(hr)&&this.modes.sort(((t,e)=>"off"===t?1:"off"===e?-1:t.localeCompare(e))),console.log(`After sort modes are ${this.modes}`),this.presets.length>0&&(this.presets=this.presets.filter((t=>t!=dr&&("cool"!=e||"frost"!=t)))),this.value={value:(null==t?void 0:t.temperature)||0,low:(null==t?void 0:t.target_temp_low)||null,high:(null==t?void 0:t.target_temp_high)||null},"on"===this.windowState||"on"===this.windowAutoState?this._hasWindow=!0:this._hasWindow=!1,"on"===this.overpoweringState?(this._hasOverpowering=!0,this.overpowering=!0):(this._hasOverpowering=!1,this.overpowering=!1),"on"===this.presenceState?(this._hasPresence=!0,this.presence=!0):(this._hasPresence=!1,this.presence=!1),"on"===this.motionState?(this._hasMotion=!0,this.motion=!0):(this._hasMotion=!1,this.motion=!1),"on"!==this.safetyState||(null===(W=null==this?void 0:this._config)||void 0===W?void 0:W.disable_safety_warning))this.safety_state=null;else{if(this.safety_state=[],this.safety_state.push({name:_n({hass:this.hass,string:"extra_states.safety_warning"}),security_msg:_n({hass:this.hass,string:"extra_states.safety_warning_msg"})}),null===(B=t.specific_states)||void 0===B?void 0:B.last_temperature_datetime){let e=wr(new Date(null===(j=t.specific_states)||void 0===j?void 0:j.last_temperature_datetime));e>0&&this.safety_state.push({name:_n({hass:this.hass,string:"extra_states.room_temp"}),security_msg:e+" "+_n({hass:this.hass,string:"extra_states.minutes"})})}if(null===(U=t.specific_states)||void 0===U?void 0:U.last_ext_temperature_datetime){let e=wr(new Date(null===(Y=t.specific_states)||void 0===Y?void 0:Y.last_ext_temperature_datetime));e>0&&this.safety_state.push({name:_n({hass:this.hass,string:"extra_states.outdoor_temp"}),security_msg:e+" "+_n({hass:this.hass,string:"extra_states.minutes"})})}}if(console.log(`Brut messages=${it}`),this.messages=[],this._hasError=!1,this.safety_state)for(const t of this.safety_state)this.messages.push(`${t.name}: ${t.security_msg}`),this._hasError=!0;for(const t of it)this.messages.push(_n({hass:this.hass,string:`extra_states.${t}`}));const nt=null==t?void 0:t.heating_failure_detection_manager,at="on"===(null==nt?void 0:nt.heating_failure_state),st="on"===(null==nt?void 0:nt.cooling_failure_state);if(at&&(this.messages.push(_n({hass:this.hass,string:"extra_states.heating_failure"})),this._hasError=!0),st&&(this.messages.push(_n({hass:this.hass,string:"extra_states.cooling_failure"})),this._hasError=!0),console.log(`Messages=${JSON.stringify(this.messages)}`),this.powerInfos=[],console.log(`MeanCyclePower=${this.meanCyclePower} PowerPercent=${this.powerPercent} ValveOpenPercent=${this.valveOpenPercent} DevicePower=${this.devicePower}`),(null===(q=this._config)||void 0===q?void 0:q.disable_power_infos)||!this.isOn&&!ot?console.log(`No power to disply is_on=${this.isOn} or disabled`):((null==t?void 0:t.is_over_switch)&&(this.meanCyclePower&&!(null===(Z=this._config)||void 0===Z?void 0:Z.powerEntity)&&this.powerInfos.push({name:"mean_power_cycle",value:xr(this.meanCyclePower,1),unit:this.meanCyclePower<7?"kW":"W",class:"vt-power-color"}),this.powerInfos.push({name:"power_percent",value:this.powerPercent,unit:"%",class:"vt-power-color"})),((null==t?void 0:t.is_over_valve)||ot)&&(this.meanCyclePower&&!(null===(X=this._config)||void 0===X?void 0:X.powerEntity)&&this.powerInfos.push({name:"mean_power_cycle",value:xr(this.meanCyclePower,1),unit:this.meanCyclePower<7?"kW":"W",class:"vt-power-color"}),this.powerInfos.push({name:"valve_open_percent",value:this.valveOpenPercent,unit:"%",class:"vt-power-color"})),(null==t?void 0:t.is_over_climate)&&!ot&&(this.devicePower&&!(null===(K=this._config)||void 0===K?void 0:K.powerEntity)&&this.powerInfos.push({name:"mean_power_cycle",value:this.devicePower,unit:this.devicePower<7?"kW":"W",class:"vt-power-color"}),this.isRegulated&&(this.powerInfos.push({name:"regulated_target_temperature",value:this.regulatedTargetTemperature,unit:null===(G=null==t?void 0:t.configuration)||void 0===G?void 0:G.temperature_unit,class:"vt-temp-color"}),this.powerInfos.push({name:"auto_regulation_mode",value:_n({hass:this.hass,string:`extra_states.${this.autoRegulationMode}`}),unit:"",class:"vt-label-color"})))),this.autoFanInfos=[],!(null===(Q=null==this?void 0:this._config)||void 0===Q?void 0:Q.disable_auto_fan_infos)&&(null==t?void 0:t.is_over_climate)&&!ot){const t=this.currentAutoFanMode!=mr?"auto_fan_mode":"auto_fan_mode_off";console.log(`VersatileThermostat UI : auto_fan icon name ${t}`),this.autoFanInfos.push({name:t,value:_n({hass:this.hass,string:`extra_states.${this.currentAutoFanMode}`}),unit:"",class:"vt-label-color"}),this.fanMode&&this.autoFanInfos.push({name:"fan_mode",value:_n({hass:this.hass,string:`extra_states.fan_${this.fanMode}`}),unit:"",class:"vt-label-color"})}this._hasAutoStartStop="hvac_off_auto_start_stop"===this.hvacOffReason,this._isAutoStartStopConfigured=!0===(null==t?void 0:t.is_auto_start_stop_configured),this._isAutoStartStopEnabled=!0===(null===(J=null==t?void 0:t.auto_start_stop_manager)||void 0===J?void 0:J.auto_start_stop_enable),console.log(`_isAutoStartStopConfigured=${this._isAutoStartStopConfigured} _isAutoStartStopEnabled=${this._isAutoStartStopEnabled} hvac_off_reason=${this.hvacOffReason}`),this._updateDisplay()}}_updateDisplay(){var t;(null===(t=null==this?void 0:this._config)||void 0===t?void 0:t.set_current_as_main)?(this._display_bottom=this._getCurrentSetpoint(),this._display_top=this.current):(this._display_bottom=this.current,this._display_top=this._getCurrentSetpoint())}_handleAction(t){this.isUserLocked||(t.currentTarget.mode===ur?this.hass.callService("versatile_thermostat","set_hvac_mode_sleep",{entity_id:this._config.entity}):this.hass.callService("climate","set_hvac_mode",{entity_id:this._config.entity,hvac_mode:t.currentTarget.mode}))}_handleDisplayMessages(){this.displayMessages=!this.displayMessages,this.requestUpdate(),this._vibrate(40)}_handlePreset(t){if(this.isUserLocked)return;this.last_target_temperature=this.temperature;const e=t.currentTarget.preset;this.timedPresetDuration&&this.timedPresetDuration>0?(this.hass.callService("versatile_thermostat","set_timed_preset",{entity_id:this._config.entity,preset:e,duration_minutes:this.timedPresetDuration}),this.timedPresetDuration=null):this.hass.callService("climate","set_preset_mode",{entity_id:this._config.entity,preset_mode:e})}_formatRemainingTime(t){if(null===t||t<=0)return"0";if(t<60)return`${t}`;const e=Math.floor(t/60),i=t%60;return 0===i?`${e}h`:`${e}h${i}`}_handleTimedPresetDurationChange(t){const e=t.target,i=e.value?parseInt(e.value,10):null;this.timedPresetDuration=i&&i>0?i:null}_handleTimedPresetSelectChange(t){const e=t.target,i=e.value?parseInt(e.value,10):null;this.timedPresetDuration=i&&i>0?i:null}_handleCancelTimedPreset(){this.isUserLocked||(console.info("VersatileThermostatUI-CARD canceling timed preset"),this.hass.callService("versatile_thermostat","cancel_timed_preset",{entity_id:this._config.entity}))}_handleClickAutoFanInfo(){if(this.isUserLocked)return;let t=mr;this.currentAutoFanMode==mr&&(t=this.autoFanMode);const e=gr[t];console.info(`VersatileThermostatUI-CARD changing auto_fan_mode to ${t} (mapped=$${e}`),e?this.hass.callService("versatile_thermostat","set_auto_fan_mode",{entity_id:this._config.entity,auto_fan_mode:e}):console.warn(`VersatileThermostatUI-CARD: auto_fan_mode ${t} has no mapping, aborting change.`)}_handleClickAutoStartStopEnable(){if(this.isUserLocked)return;let t=!this._isAutoStartStopEnabled;console.info(`VersatileThermostatUI-CARD changing auto_start_stop_enable to ${t}`),this.hass.callService("switch","toggle",{entity_id:this._config.autoStartStopEnableEntity})}_handleToggleWindowByPass(){if(this.isUserLocked)return;console.log(`_handleToggleWindowByPass called. Current windowByPass is ${this._hasWindowByPass}`);let t=!this._hasWindowByPass;console.info(`VersatileThermostatUI-CARD changing windowByPass to ${t}`),this.hass.callService("versatile_thermostat","set_window_bypass",{entity_id:this._config.entity,window_bypass:t})}_getCurrentSetpoint(){var t,e,i,o,n,a,s,r;return null!==(null===(t=null==this?void 0:this.value)||void 0===t?void 0:t.high)&&null!==(null===(e=null==this?void 0:this.value)||void 0===e?void 0:e.low)?((null===(i=null==this?void 0:this.value)||void 0===i?void 0:i.low)||0)>=this.current?(null===(o=null==this?void 0:this.value)||void 0===o?void 0:o.low)||0:((null===(n=null==this?void 0:this.value)||void 0===n?void 0:n.high)||0)<=this.current?(null===(a=null==this?void 0:this.value)||void 0===a?void 0:a.high)||0:(null===(s=null==this?void 0:this.value)||void 0===s?void 0:s.low)||0:(null===(r=null==this?void 0:this.value)||void 0===r?void 0:r.value)||0}_renderStatusText(t,e){var i;if(!(null===(i=this._config)||void 0===i?void 0:i.show_status_text)||!this.hvacAction)return j``;const o=this.hass.localize(`component.climate.entity_component._.state_attributes.hvac_action.state.${this.hvacAction}`)||this.hvacAction;return j` + + ${o} + + `}_renderHVACAction(){return this.hvacAction&&"off"!==this.hvacAction&&"idle"!==this.hvacAction?"heating"===this.hvacAction?j``:"cooling"===this.hvacAction||"cool"==this.hvacMode?j``:j``:B``}_renderHVACActionBadge(){return this.hvacAction&&"off"!==this.hvacAction&&"idle"!==this.hvacAction?"heating"===this.hvacAction?j``:"cooling"===this.hvacAction?j``:j``:B``}_renderIcon(t,e,i){if(!cr[t])return B``;const o=this.hass.localize(`component.climate.state._.${t}`)||_n({hass:this.hass,string:`extra_states.${t}`});return B` +
+ + ${(()=>{if(this.theme!==vr||this._renderingAsClassic)return B``;let t="";return this.hvacAction&&"off"!==this.hvacAction&&"idle"!==this.hvacAction?(t="heating"===this.hvacAction?"heating":"cooling"===this.hvacAction?"cooling":"warning",B`
${this._renderHVACActionBadge()}
`):B``})()} +
+ `}async _cycleHvacMode(){var t;if(!(null===(t=this._config)||void 0===t?void 0:t.entity)||!this.hass)return;const e=Array.isArray(this.modes)&&this.modes.length>0?this.modes:[];if(0===e.length)return;const i=this.hvacMode||"";let o=e.indexOf(i);-1===o&&(o=0);const n=e[(o+1)%e.length];try{n===ur?await this.hass.callService("versatile_thermostat","set_hvac_mode_sleep",{entity_id:this._config.entity}):await this.hass.callService("climate","set_hvac_mode",{entity_id:this._config.entity,hvac_mode:n})}catch(t){console.error(t)}}_renderMessagesButton(){const t=_n({hass:this.hass,string:"extra_states.messages-button"});return B` + + + `}_renderRecalculateScheduledButton(){const t=_n({hass:this.hass,string:"extra_states.recalculation_scheduled"});return B` + + + `}_renderWindowByPassButton(){const t=_n({hass:this.hass,string:"extra_states.window-bypass-button"});return B` + + + `}_renderPreset(t,e){const i=(this.hass.localize(`component.climate.state._.${t}`)||_n({hass:this.hass,string:`extra_states.${t}`}))+"\n"+_n({hass:this.hass,string:"extra_states.change_message"});return B` +
+ + +
+ `}_renderPowerInfo(t){const e=this.hass.localize(`component.climate.state._.${t.name}`)||_n({hass:this.hass,string:`extra_states.${t.name}`});return B` +
+ + + + + ${t.value} ${t.unit} +
+ `}_renderAutoFanInfo(t){const e=(this.hass.localize(`component.climate.state._.${t.name}`)||_n({hass:this.hass,string:`extra_states.${t.name}`}))+"\n"+_n({hass:this.hass,string:"extra_states.toggle_message"});return B` +
+ + + + + ${t.value} ${t.unit} +
+ `}_renderAutoStartStopEnable(){const t=_n({hass:this.hass,string:"extra_states.auto_start_stop_enable"}),e=_n({hass:this.hass,string:"extra_states.auto_start_stop_label"}),i=this._isAutoStartStopEnabled?"checked":"";return console.log(`checked=${i}`),B` +
+ + + + ${e} +
+ `}_handleMoreInfo(){Tn(this,"hass-more-info",{entityId:this._config.entity})}_handleLockToggle(){var t,e;if((null===(t=this._config)||void 0===t?void 0:t.allow_lock_toggle)&&(null===(e=this._config)||void 0===e?void 0:e.entity)&&this.hass&&this.stateObj)if(this._isLocked){if(this._hasLockCode)return this.isLocking=!1,this.showDigicodeModal=!0,void(this.enteredCode="");this._isLockConfigured?this.hass.callService("versatile_thermostat","unlock",{entity_id:this._config.entity}):(this._isLocked=this.isUserLocked=!1,this.requestUpdate())}else{if(this._hasLockCode)return this.isLocking=!0,this.showDigicodeModal=!0,void(this.enteredCode="");this._isLockConfigured?this.hass.callService("versatile_thermostat","lock",{entity_id:this._config.entity}):(this._isLocked=this.isUserLocked=!0,this.requestUpdate())}}_handleKeypadPress(t){this.enteredCode.length<4&&(this.enteredCode+=t,this.codeError=!1,4===this.enteredCode.length&&this._handleValidate())}_handleKeypadClear(){this.enteredCode=this.enteredCode.slice(0,-1),this.codeError=!1}_handleModalClose(){this.showDigicodeModal=!1,this.enteredCode="",this.codeError=!1}async _handleValidate(){if(4===this.enteredCode.length){const t=this.isLocking?"lock":"unlock";try{await this.hass.callService("versatile_thermostat",t,{entity_id:this._config.entity,code:this.enteredCode}),this._handleModalClose()}catch(t){this.codeError=!0,this.enteredCode="",this._vibrate(200)}}else this.codeError=!0,this.enteredCode="",this._vibrate(200)}buildTitle(){var t,e,i;let o="";return this._hasWindow&&!(null===(t=this._config)||void 0===t?void 0:t.disable_window)&&(o+=_n({hass:this.hass,string:"extra_states.window_open"})+"\n"),this._hasWindowByPass&&(o+=_n({hass:this.hass,string:"extra_states.window_bypass"})+"\n"),this._hasOverpowering&&!(null===(e=this._config)||void 0===e?void 0:e.disable_overpowering)&&(o+=_n({hass:this.hass,string:"extra_states.overpowering"})+"\n"),this._hasPresence&&(o+=_n({hass:this.hass,string:"extra_states.presence"})+"\n"),this._hasAutoStartStop&&!(null===(i=this._config)||void 0===i?void 0:i.disable_autoStartStop)&&(o+=_n({hass:this.hass,string:"extra_states.auto_start_stop"})+"\n"),this._hasMotion&&(o+=_n({hass:this.hass,string:"extra_states.motion"})+"\n"),o}_renderTemperature(t,e,i,o,n){var a;let s=76,r=56,l=.2;const c=e?-2:-1,d=e?-5.5:-2;let h;h=t?Mn(t,this.hass.locale,{minimumFractionDigits:1,maximumFractionDigits:1}):this.hass.localize("extra_states.temp_unavailable"),n&&e&&(s=this.effectiveDisableCircle?30:35,r=56,l=.25);const u=e?"temp-main":"temp-secondary";return j` + ${n&&!(null===(a=this._config)||void 0===a?void 0:a.disable_target_icon)?j` + `:""} + + ${j`${h}`} + + ${j` + ${this.hass.config.unit_system.temperature} + `} + + + `}constructor(){super(),this.value={},this._selectTargetTemperature="low",this.current=0,this.humidity=0,this.temperature=0,this.min=7,this.max=35,this.step=.5,this.powerPercent=0,this.meanCyclePower=0,this.valveOpenPercent=0,this.devicePower=0,this.regulatedTargetTemperature=null,this.isOn=!1,this.window=!1,this.windowByPass=!1,this.presence=!1,this.motion=!1,this.overpowering=!1,this.isDeviceActive=!1,this.isSleeping=!1,this.isRegulated=!1,this.isRecalculateScheduled=!1,this.windowState="off",this.windowAutoState="off",this.overpoweringState="off",this.presenceState="off",this.motionState="off",this.safetyState="off",this.status="loading",this.hvacMode=hr,this.hvacAction="idle",this.preset=dr,this.autoRegulationMode="",this.currentAutoFanMode=mr,this.autoFanMode=mr,this.fanMode="",this.hvacOffReason="",this.dragging=!1,this.name="",this.target="value",this._debouncedCallService=((t,e,i=!1)=>{let o;const n=(...n)=>{const a=i&&!o;clearTimeout(o),o=window.setTimeout((()=>{o=void 0,i||t(...n)}),e),a&&t(...n)};return n.cancel=()=>{clearTimeout(o)},n})((t=>this._callService(t)),1e3),this._init=!0,this._firstRender=!0,this._gunmalmgPresetScrollDone=!1,this._ignore=!1,this._hasWindow=!1,this._hasWindowByPass=!1,this._hasPresence=!1,this._hasMotion=!1,this._hasOverpowering=!1,this._hasAutoStartStop=!1,this._isAutoStartStopEnabled=!1,this._isAutoStartStopConfigured=!1,this._isLockConfigured=!1,this._isLocked=!1,this._hasLockCode=!1,this._oldValueMin=0,this._oldValueMax=0,this._display_bottom=0,this._display_top=0,this.modes=[],this.presets=[],this.safety_state={},this.powerInfos=[],this._externalPowerInfos=[],this.autoFanInfos=[],this.messages=[],this.displayMessages=!1,this._hasError=!1,this._renderingAsClassic=!1,this._showThemeMenu=!1,this._showClassicPopup=!1,this.theme="",this.isLocked=!1,this.isUserLocked=!1,this.isAutomationLocked=!1,this.showDigicodeModal=!1,this.enteredCode="",this.codeError=!1,this.isLocking=!1,this.timedPresetDuration=null,this.timedPresetActive=!1,this.timedPresetRemainingTime=null,this.timedPresetPreset=null,this._presetsPanelOpen=!1,this._regulationChartOpen=!1,this._presetTempEntities=[],this._handleKeyDown=t=>{t.key>="0"&&t.key<="9"?this._handleKeypadPress(t.key):"Backspace"===t.key&&this._handleKeypadClear()},this.render=()=>{var t,e,i,o,n;return(null===(t=this._config)||void 0===t?void 0:t.theme)===vr?(console.log("Rendering Gunmalmg theme"),nr(this)):B` + + ${(null===(o=this._config)||void 0===o?void 0:o.disable_menu)?"":B` + + ${this._showThemeMenu?B` + +
+
+
{this._handleMoreInfo(),this._closeThemeMenu()}}>${_n({hass:this.hass,string:"editor.card.climate.menu_system"})}
+
+
+ ${""} + ${(null===(n=this._config)||void 0===n?void 0:n.theme)===vr?B` +
{this._menuLockToggle(),this._closeThemeMenu()}}> + ${this._isLocked?_n({hass:this.hass,string:"extra_states.unlock"}):_n({hass:this.hass,string:"extra_states.lock"})} +
+ ${this.timedPresetActive?B` +
{this._menuCancelTimedPreset(),this._closeThemeMenu()}}> + ${_n({hass:this.hass,string:"extra_states.cancel_timed_preset"})} +
+ `:""} + `:""} +
this._applyTheme("classic")}>${_n({hass:this.hass,string:"editor.card.climate.theme_classic"})}
+
this._applyTheme("vtherm")}>${_n({hass:this.hass,string:"editor.card.climate.theme_vtherm"})}
+
this._applyTheme("uncolored")}>${_n({hass:this.hass,string:"editor.card.climate.theme_uncolored"})}
+
this._applyTheme("gunmalmg")}>${_n({hass:this.hass,string:"editor.card.climate.theme_gunmalmg"})}
+
+ `:""} + `} + + ${this._renderClassicContent()} + + +
+

+ ${this.isLocking?_n({hass:this.hass,string:"extra_states.lock"}):_n({hass:this.hass,string:"extra_states.unlock"})} ${this.name} +

+
+ + + + +
+
+ ${_n({hass:this.hass,string:"extra_states.code_error"})} +
+ +
+
+ this._handleKeypadPress("1")}>1 + this._handleKeypadPress("2")}>2 + this._handleKeypadPress("3")}>3 +
+
+ this._handleKeypadPress("4")}>4 + this._handleKeypadPress("5")}>5 + this._handleKeypadPress("6")}>6 +
+
+ this._handleKeypadPress("7")}>7 + this._handleKeypadPress("8")}>8 + this._handleKeypadPress("9")}>9 +
+
+ + this._handleKeypadPress("0")}>0 + +
+
+
+
+
+ `}}connectedCallback(){super.connectedCallback(),this._gunmalmgPresetScrollDone=!1}disconnectedCallback(){super.disconnectedCallback(),window.removeEventListener("keydown",this._handleKeyDown)}static async getConfigElement(){return await Promise.resolve().then((function(){return Bb})),document.createElement("versatile-thermostat-ui-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>["climate"].includes(t.split(".")[0]))),i=e.filter((e=>{var i;return null===(i=t.states[e].attributes)||void 0===i?void 0:i.specific_states}));return{type:"custom:versatile-thermostat-ui-card",entity:i[0]||e[0]}}};function Tr(t){return t+.5|0}kr.styles=[s` + :host { + display: block; + box-sizing: border-box; + position: relative; + container: vt-card / inline-size; + } + + ha-card { + overflow: visible; + height: 100%; + width: 100%; + vertical-align: middle; + justify-content: center; + justify-items: center; + padding-left: 1em; + padding-right: 1em; + padding-top: 1.5em; + box-sizing: border-box; + position: relative; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-content: center; + justify-content: center; + align-items: center; + } + + ha-card.locked #modes ha-icon-button, + ha-card.locked #vt-control-buttons vt-ha-outlined-icon-button, + ha-card.locked #vt-control-buttons ha-svg-icon, + ha-card.locked #presets ha-icon-button, + ha-card.locked #presets .preset-label, + ha-card.locked vt-ha-control-circular-slider, + ha-card.locked .left-info-label .auto-start-stop-enable, + ha-card.locked .left-info-label ha-icon-button { + opacity: 0.8; + pointer-events: none; + } + + ha-card.locked #presets .preset-label { + cursor: default !important; + } + + .disabled-circle-container{ + height: 145px; + width: 100%; + background: radial-gradient(var(--hvac-mode-color), transparent 50%); + } + + .disabled-circle-container.no-background-color{ + background: none; + } + + vt-ha-outlined-icon-button { + border: 1px solid var(--secondary-text-color); + border-radius: 100%; + padding: 0.5em; + cursor: pointer; + } + + .content.security_msg, vt-ha-control-circular-slider.security_msg, #modes.security_msg, #presets.security_msg, #vt-control-buttons.security_msg { + opacity: 0.5; + filter: blur(5px); + pointer-events: none; + } + + + .security, .messages { + position: absolute; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + box-sizing: border-box; + left: 0px; + top: 0px; + background: rgba(200, 200, 200, 0.16); + border-radius: var(--ha-card-border-radius,12px); + border-width: var(--ha-card-border-width,1px); + border-style: solid; + border-color: var(--ha-card-border-color,var(--divider-color,#e0e0e0)); + pointer-events: all; + font-size: 22px; + text-align: center; + line-height: 40px; + padding: 1em; + --mdc-icon-size: 40px; + z-index: 2; + } + + .messages-button.error { + color: var(--error-color) !important; + animation: pulse 2s infinite; + } + + + .unavailable { + opacity: 0.3; + } + + .unavailable #bar, .unavailable .main-value, .unavailable #value,.unavailable #current, .unavailable .current-info, + .unknown #bar, .unknown .main-value, .unknown #value,.unknown #current, .unknown .current-info { + display: none; + } + + .more-info { + position: absolute; + cursor: pointer; + top: 0px; + right: 0px; + inset-inline-end: 0px; + inset-inline-start: initial; + border-radius: 100%; + color: var(--secondary-text-color); + z-index: 3; + direction: var(--direction); + } + .container { + position: relative; + width: 100%; + height: 100%; + } + vt-ha-control-circular-slider { + --primary-color: var(--hvac-mode-color); + } + vt-ha-control-circular-slider.with-background-gradient { + z-index: 0; + position: relative; + } + vt-ha-control-circular-slider.with-background-gradient::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle, var(--hvac-mode-color) 0%, transparent 65%); + border-radius: 50%; + opacity: 0.45; + z-index: -1; + pointer-events: none; + } + + .content { + position: absolute; + width: calc(70% - 40px); + height: calc(70% - 100px); + box-sizing: border-box; + border-radius: 100%; + left: 50%; + top: calc(50% - 40px); + text-align: center; + overflow-wrap: break-word; + pointer-events: none; + display: flex; + align-items: center; + place-content: center; + flex-flow: wrap; + transform: translate(-50%,-50%); + max-width: 155px; + } + .content.no-presets { + top: calc(60% - 30px); + } + + #main { + transform: scale(2.3); + } + + .name { + display: block; + width: 100%; + text-align: center; + font-size: 20px; + word-break: keep-all; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + svg { + height: auto; + margin: auto; + display: block; + width: 100%; + -webkit-backface-visibility: hidden; + max-width: 233px; + } + + path { + stroke-linecap: round; + stroke-width: 1; + } + + text { + fill: var(--primary-text-color); + } + + /* Temperature display styles */ + .temp-main { + font-size: 15px; + } + .temp-main .uom { + font-size: 5px; + } + .temp-secondary { + font-size: 6px; + } + .temp-secondary .uom { + font-size: 3px; + } + + .auto, + .heat_cool { + --hvac-mode-color: var(--state-climate-auto-color); + } + .cool { + --mode-color: var(--label-badge-blue); + } + .heat, .heat_cool { + --mode-color: var(--label-badge-red); + } + .manual { + --mode-color: var(--state-climate-manual-color); + } + .off { + --mode-color: var(--slider-track-color); + } + .sleep { + --mode-color: #2641a3ff !important; + } + .fan_only { + --mode-color: var(--state-climate-fan_only-color); + } + .dry { + --mode-color: var(--state-climate-dry-color); + } + .idle { + --mode-color: var(--state-climate-dry-color); + } + .unknown-mode { + --mode-color: var(--state-unknown-color); + } + + .window_open { + --mode-color: #80a7c4 !important; + } + + .overpowering { + --mode-color: var(--error-color) !important; + } + + .heat_heating { + --hvac-mode-color: var(--label-badge-red); + } + + .heat_idle, .heat_off { + --hvac-mode-color: var(--label-badge-yellow); + } + + .cool_cooling { + --hvac-mode-color: var(--label-badge-blue); + } + + .cool_idle { + --hvac-mode-color:rgb(125, 194, 225); + } + + .off_off, .off_idle { + --hvac-mode-color: var(--slider-track-color); + } + + .off_heating { + --hvac-mode-color: #c7b598ff; + } + + .off_cooling { + --hvac-mode-color: #90acb8ff; + } + + + .sleep_off, .sleep_idle { + --hvac-mode-color: #2641a3ff; + } + + #modes { + z-index: 0; + position: relative; + display: flex; + width: auto; + justify-content: center; + margin-top: -3em; + margin-bottom: 1em; + } + + #presets { + z-index: 0; + position: relative; + display: flex; + width: 100%; + justify-content: center; + align-items: center; + margin-top: -1em; + margin-bottom: 1em; + } + + #presets > * { + color: var(--disabled-text-color); + } + #presets .selected-icon { + color: var(--label-badge-yellow); + } + + .timed-preset-container { + display: flex; + align-items: center; + margin-left: 10px; + padding-left: 10px; + border-left: 1px solid var(--divider-color, #e0e0e0); + } + + .timed-preset-input { + width: 30px; + height: 32px; + border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 4px; + padding: 0 8px; + font-size: 14px; + text-align: center; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + outline: none; + transition: border-color 0.2s ease; + } + + .timed-preset-input:focus { + border-color: var(--primary-color); + } + + .timed-preset-input::-webkit-outer-spin-button, + .timed-preset-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + .timed-preset-input[type=number] { + -moz-appearance: textfield; + } + + .timed-preset-label { + font-size: 12px; + color: var(--secondary-text-color); + margin-left: 4px; + } + + .timed-preset-input.active { + border-color: var(--primary-color); + background: var(--primary-color); + color: var(--text-primary-color, #fff); + } + + .timed-preset-select { + height: 32px; + border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 4px; + padding: 0 4px; + font-size: 14px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + outline: none; + cursor: pointer; + transition: border-color 0.2s ease; + } + + .timed-preset-select:focus { + border-color: var(--primary-color); + } + + .timed-preset-select.active { + border-color: var(--primary-color); + background: var(--primary-color); + color: var(--text-primary-color, #fff); + } + + .timed-preset-remaining { + font-size: 16px; + font-weight: 500; + color: var(--primary-color); + min-width: 30px; + text-align: center; + cursor: pointer; + user-select: none; + border-radius: 8px; + padding: 2px 6px; + transition: background-color 200ms ease; + } + + .timed-preset-remaining:hover { + background: rgba(var(--rgb-primary-color, 33, 150, 243), 0.12); + } + + /* Timed preset container locked state */ + .timed-preset-container.locked { + opacity: 0.45; + pointer-events: none; + user-select: none; + } + + .preset-label { + cursor: pointer; + user-select: none; + -webkit-tap-highlight-color: transparent; + display: flex; + position: relative; + align-items: center; + justify-content: flex-start; + overflow: hidden; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 5px; + padding-right: 5px; + outline: 0px; + height: 48px; + color: var(--mdc-theme-text-primary-on-background,rgba(0,0,0,.87)); + margin-left: 5px; + margin-right: 5px; + } + + #left-infos { + z-index: 3; + position: absolute; + display: block; + width: auto; + justify-content: center; + padding-bottom: 0.2em; + left: 0%; + top: 15%; + } + #left-infos.no-presets { + top: 30%; + } + + #right-lock { + z-index: 0; + position: absolute; + right: 0; + top: 15%; + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: 0.2em; + } + + #left-infos > * { + color: var(--enabled-text-color); + } + + .left-info-label { + -webkit-tap-highlight-color: transparent; + display: flex; + position: relative; + align-items: center; + justify-content: flex-start; + overflow: hidden; + padding: 0px 0px; + height: 30px; + } + + .left-info-label input { + width: 40px; + } + + #vt-control-buttons { + z-index: 0; + position: absolute; + display: block; + width: auto; + justify-content: center; + padding-bottom: 0.2em; + left: 90%; + top: 35%; + } + + #vt-control-buttons .button { + z-index: 0; + position: relative; + display: flex; + width: auto; + justify-content: center; + margin-bottom: 15px; + margin-left: -5px; + } + + #modes > * { + color: var(--disabled-text-color); + cursor: pointer; + display: inline-block; + } + #modes .selected-icon { + color: var(--mode-color); + } + + #shadowpath { + stroke: #e7e7e8; + } + + #value { + fill: var(--mode-color); + r: 5; + z-index: 4 !important; + transition: r 0.3s ease-in-out, fill 0.6s ease-in-out; + } + + #value,#current { + filter: drop-shadow(0px 0px 1px #000); + } + + #value:hover, #value:active, #value:focus, #value.active { + r: 8 !important; + } + + #current { + pointer-events: none; + fill: var(--label-badge-grey); + } + + .status { + transition: fill 0.6s ease-in-out, filter 0.6s ease-in-out; + filter: none; + } + .status.active { + fill: var(--error-color); + filter: drop-shadow(0px 0px 6px var(--error-color)); + } + + .status.cooler.active { + fill: #03A9F4; + filter: drop-shadow(0px 0px 6px #03A9F4); + } + + #bar { + stroke: var(--mode-color); + stroke-dasharray: 176; + stroke-dashoffset: 0; + transition: stroke-dashoffset 5.1s ease-in-out 0s, stroke 0.6s ease-in-out; + } + + #bar.drag { + transition: none !important; + } + #c-minus,#c-plus { + cursor: pointer; + } + .control { + cursor: pointer; + pointer-events: none; + } + ha-icon-button { + transition: color 0.6s ease-in-out; + } + .eco ha-icon-button[title="heat"], .window_open ha-icon-button[title="heat"], .overpowering ha-icon-button[title="heat"] { + --mode-color: var(--disabled-text-color); + } + + .icon-group { + cursor: pointer; + pointer-events: auto; + } + + .window { + transition: fill 0.3s ease; + fill: var(--warning-color); + cursor: pointer; + pointer-events: all; + } + + .auto-start-stop { + transition: fill 0.3s ease; + fill: var(--state-icon-color); + } + + .overpowering { + transition: fill 0.3s ease; + fill: var(--error-color); + } + .presence { + transition: fill 0.3s ease; + fill: var(--success-color); + } + .motion { + transition: fill 0.3s ease; + fill: var(--info-color); + } + + .windowByPass { + transition: fill 0.3s ease; + fill: var(--accent-color); + } + + .lock-icon { + --mdc-icon-size: 24px; + } + + .lock-icon.locked { + color: var(--error-color); + } + + .lock-icon.unlocked { + color: var(--success-color); + } + + line { + stroke: var(--disabled-text-color); + } + + ha-icon-button[title="eco"] { + --mode-color: var(--energy-non-fossil-color) !important; + } + @container vt-card (max-width: 280px) { + .content { + top: calc(50% - 10px); + } + } + @container vt-card (max-width: 255px) { + #modes { + margin-top: -2em; + } + #presets { + margin-top: -1em; + } + ha-card { + padding-top: 2em; + } + } + /* Dialog Customization */ + ha-dialog.digicode-dialog { + --mdc-dialog-min-width: 400px; + --mdc-dialog-max-width: 450px; + --mdc-theme-primary: var(--primary-color); + --mdc-theme-surface: var(--card-background-color); + } + + /* Theme menu styles */ + .theme-menu { + position: absolute; + right: 8px; + top: 36px; + background: var(--card-background-color, #fff); + border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 4px; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); + z-index: 10; + min-width: 160px; + max-height: 60vh; + overflow-y: auto; + box-sizing: border-box; + } + .theme-menu-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-bottom: 1px solid var(--divider-color, #e0e0e0); + } + .theme-menu-title { + font-weight: 600; + cursor: pointer; + } + .menu-backdrop { + position: fixed; + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: 9; + background: transparent; + } + /* theme-menu-close removed: menu header no longer contains a close button */ + .theme-menu-item { + padding: 8px 12px; + cursor: pointer; + } + .theme-menu-item:hover { background: var(--divider-color, #f1f1f1); } + + /* Theme variations ------------------------------------------------- */ + :host([theme="classic"]) vt-ha-control-circular-slider { display: block; } + :host([theme="classic"]) .disabled-circle-container { display: none !important; } + + :host([theme="vtherm"]) vt-ha-control-circular-slider { display: none !important; } + :host([theme="vtherm"]) .disabled-circle-container { display: block !important; background: radial-gradient(var(--hvac-mode-color), transparent 50%); } + + :host([theme="uncolored"]) vt-ha-control-circular-slider { display: none; } + :host([theme="uncolored"]) .disabled-circle-container { display: block !important; background: none !important; } + + /* gunmalmg theme rules moved to src/themes/gunmalmg.ts */ + + + .dialog-content { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px 0; + } + + .dialog-title { + margin-top: 0; + margin-bottom: 40px; + font-size: 24px; + font-weight: 400; + text-align: center; + } + + .error-message { + color: var(--error-color); + font-size: 14px; + height: 20px; + margin-top: 4px; + margin-bottom: 20px; + text-align: center; + opacity: 0; + transition: opacity 0.2s ease; + } + + .error-message.visible { + opacity: 1; + } + + .code-display { + display: flex; + align-items: center; + justify-content: center; + width: 200px; + padding: 20px 0; + border-bottom: 1px solid var(--secondary-text-color); + } + + .digit { + width: 12px; + height: 12px; + margin: 0 5px; + border-radius: 50%; + background-color: var(--secondary-text-color); + opacity: 0.3; + transition: all 0.2s ease; + } + + .digit.filled { + opacity: 1; + background-color: var(--primary-text-color); + transform: scale(1.2); + } + + .code-display.error { + animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; + color: var(--error-color); + } + + .keypad { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + max-width: 320px; + } + + .keypad-row { + display: flex; + justify-content: space-around; + gap: 16px; + } + + .keypad-btn { + flex: 1; + --mdc-shape-small: 4px; + --mdc-typography-button-font-size: 24px; + --mdc-typography-button-font-weight: 400; + height: 64px; + --mdc-theme-primary: var(--primary-text-color); + --mdc-button-outline-color: transparent; + display: flex; + align-items: center; + justify-content: center; + } + + .keypad-btn.spacer { + visibility: hidden; + pointer-events: none; + } + + @keyframes shake { + 10%, 90% { transform: translate3d(-1px, 0, 0); } + 20%, 80% { transform: translate3d(2px, 0, 0); } + 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } + 40%, 60% { transform: translate3d(4px, 0, 0); } + } + + ha-card.has-preset-mod { + padding-bottom: 38px; + } + + ha-card.has-regulation-chart { + padding-bottom: 38px; + } + + ha-card.has-preset-mod.has-regulation-chart { + padding-bottom: 76px; + } + + /* When bottom panels are active the card grows (auto-height/masonry layout). + top: 50% on .content shifts down by half the extra padding — compensate here. */ + ha-card.has-preset-mod .content, + ha-card.has-regulation-chart .content { + top: calc(50% - 40px - 19px); + } + + ha-card.has-preset-mod.has-regulation-chart .content { + top: calc(50% - 40px - 38px); + } + + ha-card.has-preset-mod .content.no-presets, + ha-card.has-regulation-chart .content.no-presets { + top: calc(60% - 30px - 19px); + } + + ha-card.has-preset-mod.has-regulation-chart .content.no-presets { + top: calc(60% - 30px - 38px); + } + + /* ── Container commun pour les panneaux du bas ── */ + .bottom-panels.in-card { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: var(--ha-card-background, var(--card-background-color, #fff)); + border-radius: 0 0 var(--ha-card-border-radius, 12px) var(--ha-card-border-radius, 12px); + z-index: 4; + overflow: hidden; + } + + .bottom-panels.in-popup { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: var(--ha-card-background, var(--card-background-color, #fff)); + border-radius: 0 0 12px 12px; + z-index: 4; + overflow: hidden; + } + + /* ── Preset modification collapsible panel ── */ + .preset-mod-panel { + box-sizing: border-box; + border-top: 1px solid var(--divider-color, #e0e0e0); + padding: 0 8px; + } + + .preset-mod-header { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + padding: 6px 4px; + user-select: none; + color: var(--secondary-text-color); + } + + .preset-mod-header:hover { + color: var(--primary-text-color); + } + + .preset-mod-title { + font-size: 12px; + font-weight: 500; + letter-spacing: 0.03em; + text-transform: uppercase; + } + + .preset-mod-chevron { + --mdc-icon-size: 18px; + } + + .preset-mod-chevron.open { + /* no rotation - icon is swapped directly */ + } + + .preset-mod-body { + padding: 4px 0 8px 0; + animation: fadeIn 0.15s ease; + } + + @keyframes fadeIn { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } + } + + .preset-mod-empty { + font-size: 12px; + color: var(--secondary-text-color); + text-align: center; + padding: 8px; + } + + .preset-temp-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + } + + .preset-temp-table th, + .preset-temp-table td { + padding: 3px 4px; + text-align: center; + vertical-align: middle; + } + + .preset-temp-col-label { + font-weight: 600; + color: var(--secondary-text-color); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.04em; + } + + .preset-temp-row-label { + text-align: left !important; + color: var(--secondary-text-color); + font-size: 11px; + white-space: nowrap; + padding-right: 6px; + } + + .preset-temp-cell.empty { + color: var(--disabled-text-color, #bbb); + } + + .preset-temp-input { + width: 40px; + padding: 2px 1px; + border: 1px solid var(--divider-color, #ccc); + border-right: none; + border-radius: 4px 0 0 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 13px; + text-align: center; + -moz-appearance: textfield; + height: 40px; + box-sizing: border-box; + } + + .preset-temp-input::-webkit-outer-spin-button, + .preset-temp-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + .preset-temp-input:focus { + outline: none; + border-color: var(--primary-color); + } + + /* Stepper: input + vertical ± buttons */ + .preset-temp-stepper { + display: inline-flex; + flex-direction: row; + align-items: stretch; + border-radius: 4px; + overflow: hidden; + } + + /* Vertical stack for + and − */ + .preset-step-btns { + display: flex; + flex-direction: column; + width: 24px; + flex-shrink: 0; + } + + .preset-step-btn { + flex: 1; + padding: 0; + border: 1px solid var(--divider-color, #ccc); + border-left: none; + background: var(--secondary-background-color, #f5f5f5); + color: var(--primary-text-color); + font-size: 14px; + font-weight: 700; + cursor: pointer; + user-select: none; + line-height: 1; + transition: background-color 150ms ease; + display: flex; + align-items: center; + justify-content: center; + min-height: 19px; + } + + .preset-step-up { + border-bottom: 1px solid var(--divider-color, #ccc); + border-radius: 0 4px 0 0; + } + + .preset-step-down { + border-top: none; + border-radius: 0 0 4px 0; + } + + .preset-step-btn:hover { + background: var(--primary-color); + color: white; + } + + .preset-step-btn:active { + opacity: 0.75; + } + + /* Column colors */ + .preset-temp-col-label.col-frost { color: #3a9ff2; } + .preset-temp-col-label.col-eco { color: #5dd461; } + .preset-temp-col-label.col-comfort { color: #f9a21f; } + .preset-temp-col-label.col-boost { color: #f75252; } + + .preset-col-frost .preset-step-btn { background: rgba(58,159,242,0.15); color: #3a9ff2; border-color: rgba(58,159,242,0.4); } + .preset-col-frost .preset-step-btn:hover { background: #3a9ff2; color: white; } + .preset-col-frost .preset-step-up { border-bottom-color: rgba(58,159,242,0.4); } + .preset-col-frost .preset-temp-input { border-color: rgba(58,159,242,0.4); } + + .preset-col-eco .preset-step-btn { background: rgba(93,212,97,0.15); color: #5dd461; border-color: rgba(93,212,97,0.4); } + .preset-col-eco .preset-step-btn:hover { background: #5dd461; color: white; } + .preset-col-eco .preset-step-up { border-bottom-color: rgba(93,212,97,0.4); } + .preset-col-eco .preset-temp-input { border-color: rgba(93,212,97,0.4); } + + .preset-col-comfort .preset-step-btn { background: rgba(249,162,31,0.15); color: #f9a21f; border-color: rgba(249,162,31,0.4); } + .preset-col-comfort .preset-step-btn:hover { background: #f9a21f; color: white; } + .preset-col-comfort .preset-step-up { border-bottom-color: rgba(249,162,31,0.4); } + .preset-col-comfort .preset-temp-input { border-color: rgba(249,162,31,0.4); } + + .preset-col-boost .preset-step-btn { background: rgba(247,82,82,0.15); color: #f75252; border-color: rgba(247,82,82,0.4); } + .preset-col-boost .preset-step-btn:hover { background: #f75252; color: white; } + .preset-col-boost .preset-step-up { border-bottom-color: rgba(247,82,82,0.4); } + .preset-col-boost .preset-temp-input { border-color: rgba(247,82,82,0.4); } + + /* Preset modification panel locked state */ + .preset-mod-panel.locked { + opacity: 0.45; + pointer-events: none; + user-select: none; + } + + /* ── Regulation chart collapsible panel ── */ + .regulation-chart-panel { + box-sizing: border-box; + border-top: 1px solid var(--divider-color, #e0e0e0); + padding: 0 8px; + } + + .regulation-chart-header { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + padding: 6px 4px; + user-select: none; + color: var(--secondary-text-color); + } + + .regulation-chart-header:hover { + color: var(--primary-text-color); + } + + .regulation-chart-title { + font-size: 12px; + font-weight: 500; + letter-spacing: 0.03em; + text-transform: uppercase; + } + + .regulation-chart-chevron { + --mdc-icon-size: 18px; + } + + .regulation-chart-body { + padding: 4px 0 8px 0; + min-height: 60px; + animation: fadeIn 0.15s ease; + } + } + + @keyframes shake { + 10%, 90% { transform: translate3d(-1px, 0, 0); } + 20%, 80% { transform: translate3d(2px, 0, 0); } + 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } + 40%, 60% { transform: translate3d(4px, 0, 0); } + } + + .hvac-action-text-svg { + text-anchor: middle; + font-weight: var(--ha-font-weight-light); + font-size: 6px; + text-align: center; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: var(--ha-line-height-normal); + min-height: 1.5em; + white-space: nowrap; + filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.8)); + } + .hvac-action-text-svg.heating { + fill: var(--hvac-mode-color, #f44336); + } + + `,ar,sr],kr.additionalThemeStyles=[sr],t([ut({attribute:!1})],kr.prototype,"hass",void 0),t([ut({type:Number})],kr.prototype,"value",void 0),t([mt()],kr.prototype,"_selectTargetTemperature",void 0),t([ut({type:Number})],kr.prototype,"current",void 0),t([ut({type:Number})],kr.prototype,"humidity",void 0),t([ut({type:Number})],kr.prototype,"temperature",void 0),t([ut({type:Number})],kr.prototype,"min",void 0),t([ut({type:Number})],kr.prototype,"max",void 0),t([ut({type:Number})],kr.prototype,"step",void 0),t([ut({type:Number})],kr.prototype,"powerPercent",void 0),t([ut({type:Number})],kr.prototype,"meanCyclePower",void 0),t([ut({type:Number})],kr.prototype,"valveOpenPercent",void 0),t([ut({type:Number})],kr.prototype,"devicePower",void 0),t([ut({type:Number})],kr.prototype,"regulatedTargetTemperature",void 0),t([ut({type:Boolean})],kr.prototype,"isOn",void 0),t([ut({type:Boolean})],kr.prototype,"window",void 0),t([ut({type:Boolean})],kr.prototype,"windowByPass",void 0),t([ut({type:Boolean})],kr.prototype,"presence",void 0),t([ut({type:Boolean})],kr.prototype,"motion",void 0),t([ut({type:Boolean})],kr.prototype,"overpowering",void 0),t([ut({type:Boolean})],kr.prototype,"isDeviceActive",void 0),t([ut({type:Boolean})],kr.prototype,"isSleeping",void 0),t([ut({type:Boolean})],kr.prototype,"isRegulated",void 0),t([ut({type:Boolean})],kr.prototype,"isRecalculateScheduled",void 0),t([ut({type:String})],kr.prototype,"windowState",void 0),t([ut({type:String})],kr.prototype,"windowAutoState",void 0),t([ut({type:String})],kr.prototype,"overpoweringState",void 0),t([ut({type:String})],kr.prototype,"presenceState",void 0),t([ut({type:String})],kr.prototype,"motionState",void 0),t([ut({type:String})],kr.prototype,"safetyState",void 0),t([ut({type:String})],kr.prototype,"status",void 0),t([ut({type:String})],kr.prototype,"hvacMode",void 0),t([ut({type:String})],kr.prototype,"hvacAction",void 0),t([ut({type:String})],kr.prototype,"preset",void 0),t([ut({type:String})],kr.prototype,"autoRegulationMode",void 0),t([ut({type:String})],kr.prototype,"currentAutoFanMode",void 0),t([ut({type:String})],kr.prototype,"autoFanMode",void 0),t([ut({type:String})],kr.prototype,"fanMode",void 0),t([ut({type:String})],kr.prototype,"hvacOffReason",void 0),t([ut({type:Boolean,reflect:!0})],kr.prototype,"dragging",void 0),t([ut({type:String})],kr.prototype,"name",void 0),t([mt()],kr.prototype,"changingHigh",void 0),t([mt()],kr.prototype,"_hasError",void 0),t([mt()],kr.prototype,"_config",void 0),t([mt()],kr.prototype,"_showThemeMenu",void 0),t([mt()],kr.prototype,"_showClassicPopup",void 0),t([ut({type:String})],kr.prototype,"theme",void 0),t([mt()],kr.prototype,"isLocked",void 0),t([mt()],kr.prototype,"isUserLocked",void 0),t([mt()],kr.prototype,"isAutomationLocked",void 0),t([mt()],kr.prototype,"showDigicodeModal",void 0),t([mt()],kr.prototype,"enteredCode",void 0),t([mt()],kr.prototype,"codeError",void 0),t([mt()],kr.prototype,"isLocking",void 0),t([mt()],kr.prototype,"timedPresetDuration",void 0),t([mt()],kr.prototype,"timedPresetActive",void 0),t([mt()],kr.prototype,"timedPresetRemainingTime",void 0),t([mt()],kr.prototype,"timedPresetPreset",void 0),t([mt()],kr.prototype,"_presetsPanelOpen",void 0),t([mt()],kr.prototype,"_regulationChartOpen",void 0),t([mt()],kr.prototype,"_presetTempEntities",void 0),kr=t([ct("versatile-thermostat-ui-card")],kr);const Sr=(t,e,i)=>Math.max(Math.min(t,i),e);function Mr(t){return Sr(Tr(2.55*t),0,255)}function Dr(t){return Sr(Tr(255*t),0,255)}function Cr(t){return Sr(Tr(t/2.55)/100,0,1)}function Ar(t){return Sr(Tr(100*t),0,100)}const Pr={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Er=[..."0123456789ABCDEF"],Lr=t=>Er[15&t],$r=t=>Er[(240&t)>>4]+Er[15&t],zr=t=>(240&t)>>4==(15&t);function Or(t){var e=(t=>zr(t.r)&&zr(t.g)&&zr(t.b)&&zr(t.a))(t)?Lr:$r;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Vr=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Hr(t,e,i){const o=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-o*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Rr(t,e,i){const o=(o,n=(o+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[o(5),o(3),o(1)]}function Ir(t,e,i){const o=Hr(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)o[n]*=1-e-i,o[n]+=e;return o}function Nr(t){const e=t.r/255,i=t.g/255,o=t.b/255,n=Math.max(e,i,o),a=Math.min(e,i,o),s=(n+a)/2;let r,l,c;return n!==a&&(c=n-a,l=s>.5?c/(2-n-a):c/(n+a),r=function(t,e,i,o,n){return t===n?(e-i)/o+(e>16&255,a>>8&255,255&a]}return t}(),qr.transparent=[0,0,0,0]);const e=qr[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const Xr=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Kr=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Gr=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Qr(t,e,i){if(t){let o=Nr(t);o[e]=Math.max(0,Math.min(o[e]+o[e]*i,0===e?360:1)),o=Wr(o),t.r=o[0],t.g=o[1],t.b=o[2]}}function Jr(t,e){return t?Object.assign(e||{},t):t}function tl(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Dr(t[3]))):(e=Jr(t,{r:0,g:0,b:0,a:1})).a=Dr(e.a),e}function el(t){return"r"===t.charAt(0)?function(t){const e=Xr.exec(t);let i,o,n,a=255;if(e){if(e[7]!==i){const t=+e[7];a=e[8]?Mr(t):Sr(255*t,0,255)}return i=+e[1],o=+e[3],n=+e[5],i=255&(e[2]?Mr(i):Sr(i,0,255)),o=255&(e[4]?Mr(o):Sr(o,0,255)),n=255&(e[6]?Mr(n):Sr(n,0,255)),{r:i,g:o,b:n,a:a}}}(t):jr(t)}class il{constructor(t){if(t instanceof il)return t;const e=typeof t;let i;var o,n,a;"object"===e?i=tl(t):"string"===e&&(a=(o=t).length,"#"===o[0]&&(4===a||5===a?n={r:255&17*Pr[o[1]],g:255&17*Pr[o[2]],b:255&17*Pr[o[3]],a:5===a?17*Pr[o[4]]:255}:7!==a&&9!==a||(n={r:Pr[o[1]]<<4|Pr[o[2]],g:Pr[o[3]]<<4|Pr[o[4]],b:Pr[o[5]]<<4|Pr[o[6]],a:9===a?Pr[o[7]]<<4|Pr[o[8]]:255})),i=n||Zr(t)||el(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=Jr(this._rgb);return t&&(t.a=Cr(t.a)),t}set rgb(t){this._rgb=tl(t)}rgbString(){return this._valid?function(t){return t&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${Cr(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`)}(this._rgb):void 0}hexString(){return this._valid?Or(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=Nr(t),i=e[0],o=Ar(e[1]),n=Ar(e[2]);return t.a<255?`hsla(${i}, ${o}%, ${n}%, ${Cr(t.a)})`:`hsl(${i}, ${o}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,o=t.rgb;let n;const a=e===n?.5:e,s=2*a-1,r=i.a-o.a,l=((s*r==-1?s:(s+r)/(1+s*r))+1)/2;n=1-l,i.r=255&l*i.r+n*o.r+.5,i.g=255&l*i.g+n*o.g+.5,i.b=255&l*i.b+n*o.b+.5,i.a=a*i.a+(1-a)*o.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const o=Gr(Cr(t.r)),n=Gr(Cr(t.g)),a=Gr(Cr(t.b));return{r:Dr(Kr(o+i*(Gr(Cr(e.r))-o))),g:Dr(Kr(n+i*(Gr(Cr(e.g))-n))),b:Dr(Kr(a+i*(Gr(Cr(e.b))-a))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new il(this.rgb)}alpha(t){return this._rgb.a=Dr(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=Tr(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Qr(this._rgb,2,t),this}darken(t){return Qr(this._rgb,2,-t),this}saturate(t){return Qr(this._rgb,1,t),this}desaturate(t){return Qr(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=Nr(t);i[0]=Br(i[0]+e),i=Wr(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function ol(){}const nl=(()=>{let t=0;return()=>t++})();function al(t){return null==t}function sl(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function rl(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function ll(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function cl(t,e){return ll(t)?t:e}function dl(t,e){return void 0===t?e:t}const hl=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function ul(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function ml(t,e,i,o){let n,a,s;if(sl(t))if(a=t.length,o)for(n=a-1;n>=0;n--)e.call(i,t[n],n);else for(n=0;nt,x:t=>t.x,y:t=>t.y};function xl(t,e){const i=wl[e]||(wl[e]=function(t){const e=function(t){const e=t.split("."),i=[];let o="";for(const t of e)o+=t,o.endsWith("\\")?o=o.slice(0,-1)+".":(i.push(o),o="");return i}(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function kl(t){return t.charAt(0).toUpperCase()+t.slice(1)}const Tl=t=>void 0!==t,Sl=t=>"function"==typeof t,Ml=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};const Dl=Math.PI,Cl=2*Dl,Al=Cl+Dl,Pl=Number.POSITIVE_INFINITY,El=Dl/180,Ll=Dl/2,$l=Dl/4,zl=2*Dl/3,Ol=Math.log10,Vl=Math.sign;function Hl(t,e,i){return Math.abs(t-e)l&&c=Math.min(e,i)-o&&t<=Math.max(e,i)+o}function Gl(t,e,i){i=i||(i=>t[i]1;)o=a+n>>1,i(o)?a=o:n=o;return{lo:a,hi:n}}const Ql=(t,e,i,o)=>Gl(t,i,o?o=>{const n=t[o][e];return nt[o][e]Gl(t,i,(o=>t[o][e]>=i));const tc=["push","pop","shift","splice","unshift"];function ec(t,e){const i=t._chartjs;if(!i)return;const o=i.listeners,n=o.indexOf(e);-1!==n&&o.splice(n,1),o.length>0||(tc.forEach((e=>{delete t[e]})),delete t._chartjs)}function ic(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const oc="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function nc(t,e){let i=[],o=!1;return function(...n){i=n,o||(o=!0,oc.call(window,(()=>{o=!1,t.apply(e,i)})))}}const ac=t=>"start"===t?"left":"end"===t?"right":"center",sc=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2;function rc(t,e,i){const o=e.length;let n=0,a=o;if(t._sorted){const{iScale:s,vScale:r,_parsed:l}=t,c=t.dataset&&t.dataset.options?t.dataset.options.spanGaps:null,d=s.axis,{min:h,max:u,minDefined:m,maxDefined:p}=s.getUserBounds();if(m){if(n=Math.min(Ql(l,d,h).lo,i?o:Ql(e,d,s.getPixelForValue(h)).lo),c){const t=l.slice(0,n+1).reverse().findIndex((t=>!al(t[r.axis])));n-=Math.max(0,t)}n=Xl(n,0,o-1)}if(p){let t=Math.max(Ql(l,s.axis,u,!0).hi+1,i?0:Ql(e,d,s.getPixelForValue(u),!0).hi+1);if(c){const e=l.slice(t-1).findIndex((t=>!al(t[r.axis])));t+=Math.max(0,e)}a=Xl(t,n,o)-n}else a=o-n}return{start:n,count:a}}function lc(t){const{xScale:e,yScale:i,_scaleRanges:o}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!o)return t._scaleRanges=n,!0;const a=o.xmin!==e.min||o.xmax!==e.max||o.ymin!==i.min||o.ymax!==i.max;return Object.assign(o,n),a}const cc=t=>0===t||1===t,dc=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*Cl/i),hc=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*Cl/i)+1,uc={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*Ll),easeOutSine:t=>Math.sin(t*Ll),easeInOutSine:t=>-.5*(Math.cos(Dl*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>cc(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>cc(t)?t:dc(t,.075,.3),easeOutElastic:t=>cc(t)?t:hc(t,.075,.3),easeInOutElastic(t){const e=.1125;return cc(t)?t:t<.5?.5*dc(2*t,e,.45):.5+.5*hc(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-uc.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*uc.easeInBounce(2*t):.5*uc.easeOutBounce(2*t-1)+.5};function mc(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function pc(t){return mc(t)?t:new il(t)}function _c(t){return mc(t)?t:new il(t).saturate(.5).darken(.1).hexString()}const gc=["x","y","borderWidth","radius","tension"],fc=["color","borderColor","backgroundColor"];const bc=new Map;function vc(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let o=bc.get(i);return o||(o=new Intl.NumberFormat(t,e),bc.set(i,o)),o}(e,i).format(t)}const yc={values:t=>sl(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const o=this.chart.options.locale;let n,a=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),a=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const s=Ol(Math.abs(a)),r=isNaN(s)?1:Math.max(Math.min(-1*Math.floor(s),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),vc(t,o,l)},logarithmic(t,e,i){if(0===t)return"0";const o=i[e].significand||t/Math.pow(10,Math.floor(Ol(t)));return[1,2,3,5,10,15].includes(o)||e>.8*i.length?yc.numeric.call(this,t,e,i):""}};var wc={formatters:yc};const xc=Object.create(null),kc=Object.create(null);function Tc(t,e){if(!e)return t;const i=e.split(".");for(let e=0,o=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>_c(e.backgroundColor),this.hoverBorderColor=(t,e)=>_c(e.borderColor),this.hoverColor=(t,e)=>_c(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return Sc(this,t,e)}get(t){return Tc(this,t)}describe(t,e){return Sc(kc,t,e)}override(t,e){return Sc(xc,t,e)}route(t,e,i,o){const n=Tc(this,t),a=Tc(this,i),s="_"+e;Object.defineProperties(n,{[s]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[s],e=a[o];return rl(t)?Object.assign({},e,t):dl(t,e)},set(t){this[s]=t}}})}apply(t){t.forEach((t=>t(this)))}}var Dc=new Mc({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:fc},numbers:{type:"number",properties:gc}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:wc.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function Cc(t,e,i,o,n){let a=e[n];return a||(a=e[n]=t.measureText(n).width,i.push(n)),a>o&&(o=a),o}function Ac(t,e,i,o){let n=(o=o||{}).data=o.data||{},a=o.garbageCollect=o.garbageCollect||[];o.font!==e&&(n=o.data={},a=o.garbageCollect=[],o.font=e),t.save(),t.font=e;let s=0;const r=i.length;let l,c,d,h,u;for(l=0;li.length){for(l=0;l0&&t.stroke()}}function zc(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==a.strokeColor;let l,c;for(t.save(),t.font=n.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),al(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,a),l=0;l+t||0;function qc(t,e){const i={},o=rl(e),n=o?Object.keys(e):e,a=rl(t)?o?i=>dl(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=Yc(a(t));return i}function Zc(t){return qc(t,{top:"y",right:"x",bottom:"y",left:"x"})}function Xc(t){return qc(t,["topLeft","topRight","bottomLeft","bottomRight"])}function Kc(t){const e=Zc(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Gc(t,e){t=t||{},e=e||Dc.font;let i=dl(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let o=dl(t.style,e.style);o&&!(""+o).match(jc)&&(console.warn('Invalid font style specified: "'+o+'"'),o=void 0);const n={family:dl(t.family,e.family),lineHeight:Uc(dl(t.lineHeight,e.lineHeight),i),size:i,style:o,weight:dl(t.weight,e.weight),string:""};return n.string=function(t){return!t||al(t.size)||al(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}(n),n}function Qc(t,e,i,o){let n,a,s,r=!0;for(n=0,a=t.length;nt[0])){const a=i||t;void 0===o&&(o=hd("_fallback",t));const s={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:a,_fallback:o,_getTarget:n,override:i=>td([i,...t],e,a,o)};return new Proxy(s,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,o)=>ad(i,o,(()=>function(t,e,i,o){let n;for(const a of e)if(n=hd(od(a,t),i),void 0!==n)return nd(t,n)?cd(i,o,t,n):n}(o,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ud(t).includes(e),ownKeys:t=>ud(t),set(t,e,i){const o=t._storage||(t._storage=n());return t[e]=o[e]=i,delete t._keys,!0}})}function ed(t,e,i,o){const n={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:id(t,o),setContext:e=>ed(t,e,i,o),override:n=>ed(t.override(n),e,i,o)};return new Proxy(n,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>ad(t,e,(()=>function(t,e,i){const{_proxy:o,_context:n,_subProxy:a,_descriptors:s}=t;let r=o[e];Sl(r)&&s.isScriptable(e)&&(r=function(t,e,i,o){const{_proxy:n,_context:a,_subProxy:s,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t);let l=e(a,s||o);r.delete(t),nd(t,l)&&(l=cd(n._scopes,n,t,l));return l}(e,r,t,i));sl(r)&&r.length&&(r=function(t,e,i,o){const{_proxy:n,_context:a,_subProxy:s,_descriptors:r}=i;if(void 0!==a.index&&o(t))return e[a.index%e.length];if(rl(e[0])){const i=e,o=n._scopes.filter((t=>t!==i));e=[];for(const l of i){const i=cd(o,n,t,l);e.push(ed(i,a,s&&s[t],r))}}return e}(e,r,t,s.isIndexable));nd(e,r)&&(r=ed(r,n,a&&a[e],s));return r}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,o)=>(t[i]=o,delete e[i],!0)})}function id(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:o=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:o,isScriptable:Sl(i)?i:()=>i,isIndexable:Sl(o)?o:()=>o}}const od=(t,e)=>t?t+kl(e):e,nd=(t,e)=>rl(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function ad(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e)||"constructor"===e)return t[e];const o=i();return t[e]=o,o}function sd(t,e,i){return Sl(t)?t(e,i):t}const rd=(t,e)=>!0===t?e:"string"==typeof t?xl(e,t):void 0;function ld(t,e,i,o,n){for(const a of e){const e=rd(i,a);if(e){t.add(e);const a=sd(e._fallback,i,n);if(void 0!==a&&a!==i&&a!==o)return a}else if(!1===e&&void 0!==o&&i!==o)return null}return!1}function cd(t,e,i,o){const n=e._rootScopes,a=sd(e._fallback,i,o),s=[...t,...n],r=new Set;r.add(o);let l=dd(r,s,i,a||i,o);return null!==l&&((void 0===a||a===i||(l=dd(r,s,a,l,o),null!==l))&&td(Array.from(r),[""],n,a,(()=>function(t,e,i){const o=t._getTarget();e in o||(o[e]={});const n=o[e];if(sl(n)&&rl(i))return i;return n||{}}(e,i,o))))}function dd(t,e,i,o,n){for(;i;)i=ld(t,e,i,o,n);return i}function hd(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ud(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function md(t,e,i,o){const{iScale:n}=t,{key:a="r"}=this._parsing,s=new Array(o);let r,l,c,d;for(r=0,l=o;re"x"===t?"y":"x";function fd(t,e,i,o){const n=t.skip?e:t,a=e,s=i.skip?e:i,r=Ul(a,n),l=Ul(s,a);let c=r/(r+l),d=l/(r+l);c=isNaN(c)?0:c,d=isNaN(d)?0:d;const h=o*c,u=o*d;return{previous:{x:a.x-h*(s.x-n.x),y:a.y-h*(s.y-n.y)},next:{x:a.x+u*(s.x-n.x),y:a.y+u*(s.y-n.y)}}}function bd(t,e="x"){const i=gd(e),o=t.length,n=Array(o).fill(0),a=Array(o);let s,r,l,c=_d(t,0);for(s=0;s!t.skip))),"monotone"===e.cubicInterpolationMode)bd(t,n);else{let i=o?t[t.length-1]:t[0];for(a=0,s=t.length;at.ownerDocument.defaultView.getComputedStyle(t,null);const Sd=["top","right","bottom","left"];function Md(t,e,i){const o={};i=i?"-"+i:"";for(let n=0;n<4;n++){const a=Sd[n];o[a]=parseFloat(t[e+"-"+a+i])||0}return o.width=o.left+o.right,o.height=o.top+o.bottom,o}const Dd=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function Cd(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:o}=e,n=Td(i),a="border-box"===n.boxSizing,s=Md(n,"padding"),r=Md(n,"border","width"),{x:l,y:c,box:d}=function(t,e){const i=t.touches,o=i&&i.length?i[0]:t,{offsetX:n,offsetY:a}=o;let s,r,l=!1;if(Dd(n,a,t.target))s=n,r=a;else{const t=e.getBoundingClientRect();s=o.clientX-t.left,r=o.clientY-t.top,l=!0}return{x:s,y:r,box:l}}(t,i),h=s.left+(d&&r.left),u=s.top+(d&&r.top);let{width:m,height:p}=e;return a&&(m-=s.width+r.width,p-=s.height+r.height),{x:Math.round((l-h)/m*i.width/o),y:Math.round((c-u)/p*i.height/o)}}const Ad=t=>Math.round(10*t)/10;function Pd(t,e,i,o){const n=Td(t),a=Md(n,"margin"),s=kd(n.maxWidth,t,"clientWidth")||Pl,r=kd(n.maxHeight,t,"clientHeight")||Pl,l=function(t,e,i){let o,n;if(void 0===e||void 0===i){const a=t&&xd(t);if(a){const t=a.getBoundingClientRect(),s=Td(a),r=Md(s,"border","width"),l=Md(s,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,o=kd(s.maxWidth,a,"clientWidth"),n=kd(s.maxHeight,a,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:o||Pl,maxHeight:n||Pl}}(t,e,i);let{width:c,height:d}=l;if("content-box"===n.boxSizing){const t=Md(n,"border","width"),e=Md(n,"padding");c-=e.width+t.width,d-=e.height+t.height}c=Math.max(0,c-a.width),d=Math.max(0,o?c/o:d-a.height),c=Ad(Math.min(c,s,l.maxWidth)),d=Ad(Math.min(d,r,l.maxHeight)),c&&!d&&(d=Ad(c/2));return(void 0!==e||void 0!==i)&&o&&l.height&&d>l.height&&(d=l.height,c=Ad(Math.floor(d*o))),{width:c,height:d}}function Ed(t,e,i){const o=e||1,n=Ad(t.height*o),a=Ad(t.width*o);t.height=Ad(t.height),t.width=Ad(t.width);const s=t.canvas;return s.style&&(i||!s.style.height&&!s.style.width)&&(s.style.height=`${t.height}px`,s.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==o||s.height!==n||s.width!==a)&&(t.currentDevicePixelRatio=o,s.height=n,s.width=a,t.ctx.setTransform(o,0,0,o,0,0),!0)}const Ld=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};wd()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function $d(t,e){const i=function(t,e){return Td(t).getPropertyValue(e)}(t,e),o=i&&i.match(/^(\d+)(\.\d+)?px$/);return o?+o[1]:void 0}function zd(t,e,i,o){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function Od(t,e,i,o){return{x:t.x+i*(e.x-t.x),y:"middle"===o?i<.5?t.y:e.y:"after"===o?i<1?t.y:e.y:i>0?e.y:t.y}}function Vd(t,e,i,o){const n={x:t.cp2x,y:t.cp2y},a={x:e.cp1x,y:e.cp1y},s=zd(t,n,i),r=zd(n,a,i),l=zd(a,e,i),c=zd(s,r,i),d=zd(r,l,i);return zd(c,d,i)}function Hd(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Rd(t,e){let i,o;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,o=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=o)}function Id(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Nd(t){return"angle"===t?{between:Zl,compare:Yl,normalize:ql}:{between:Kl,compare:(t,e)=>t-e,normalize:t=>t}}function Fd({start:t,end:e,count:i,loop:o,style:n}){return{start:t%i,end:e%i,loop:o&&(e-t+1)%i==0,style:n}}function Wd(t,e,i){if(!i)return[t];const{property:o,start:n,end:a}=i,s=e.length,{compare:r,between:l,normalize:c}=Nd(o),{start:d,end:h,loop:u,style:m}=function(t,e,i){const{property:o,start:n,end:a}=i,{between:s,normalize:r}=Nd(o),l=e.length;let c,d,{start:h,end:u,loop:m}=t;if(m){for(h+=l,u+=l,c=0,d=l;cb||l(n,f,_)&&0!==r(n,f),w=()=>!b||0===r(a,_)||l(a,f,_);for(let t=d,i=d;t<=h;++t)g=e[t%s],g.skip||(_=c(g[o]),_!==f&&(b=l(_,n,a),null===v&&y()&&(v=0===r(_,n)?t:i),null!==v&&w()&&(p.push(Fd({start:v,end:t,loop:u,count:s,style:m})),v=null),i=t,f=_));return null!==v&&p.push(Fd({start:v,end:h,loop:u,count:s,style:m})),p}function Bd(t,e){const i=[],o=t.segments;for(let n=0;no({chart:t,initial:e.initial,numSteps:a,currentStep:Math.min(i-e.start,a)})))}_refresh(){this._request||(this._running=!0,this._request=oc.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,o)=>{if(!i.running||!i.items.length)return;const n=i.items;let a,s=n.length-1,r=!1;for(;s>=0;--s)a=n[s],a._active?(a._total>i.duration&&(i.duration=a._total),a.tick(t),r=!0):(n[s]=n[n.length-1],n.pop());r&&(o.draw(),this._notify(o,i,t,"progress")),n.length||(i.running=!1,this._notify(o,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let o=i.length-1;for(;o>=0;--o)i[o].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var Kd=new Xd;const Gd="transparent",Qd={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const o=pc(t||Gd),n=o.valid&&pc(e||Gd);return n&&n.valid?n.mix(o,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class Jd{constructor(t,e,i,o){const n=e[i];o=Qc([t.to,o,n,t.from]);const a=Qc([t.from,n,o]);this._active=!0,this._fn=t.fn||Qd[t.type||typeof a],this._easing=uc[t.easing]||uc.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=a,this._to=o,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const o=this._target[this._prop],n=i-this._start,a=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(a,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Qc([t.to,e,o,t.from]),this._from=Qc([t.from,o,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,o=this._prop,n=this._from,a=this._loop,s=this._to;let r;if(this._active=n!==s&&(a||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[o]=this._fn(n,s,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const n=t[o];if(!rl(n))return;const a={};for(const t of e)a[t]=n[t];(sl(n.properties)&&n.properties||[o]).forEach((t=>{t!==o&&i.has(t)||i.set(t,a)}))}))}_animateOptions(t,e){const i=e.options,o=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!o)return[];const n=this._createAnimations(o,i);return i.$shared&&function(t,e){const i=[],o=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,o=[],n=t.$animations||(t.$animations={}),a=Object.keys(e),s=Date.now();let r;for(r=a.length-1;r>=0;--r){const l=a[r];if("$"===l.charAt(0))continue;if("options"===l){o.push(...this._animateOptions(t,e));continue}const c=e[l];let d=n[l];const h=i.get(l);if(d){if(h&&d.active()){d.update(h,c,s);continue}d.cancel()}h&&h.duration?(n[l]=d=new Jd(h,t,l,c),o.push(d)):t[l]=c}return o}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(Kd.add(this._chart,i),!0):void 0}}function eh(t,e){const i=t&&t.options||{},o=i.reverse,n=void 0===i.min?e:0,a=void 0===i.max?e:0;return{start:o?a:n,end:o?n:a}}function ih(t,e){const i=[],o=t._getSortedDatasetMetas(e);let n,a;for(n=0,a=o.length;n0||!i&&e<0)return n.index}return null}function rh(t,e){const{chart:i,_cachedMeta:o}=t,n=i._stacks||(i._stacks={}),{iScale:a,vScale:s,index:r}=o,l=a.axis,c=s.axis,d=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(a,s,o),h=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function ch(t,e){const i=t.controller.index,o=t.vScale&&t.vScale.axis;if(o){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[o]||void 0===e[o][i])return;delete e[o][i],void 0!==e[o]._visualValues&&void 0!==e[o]._visualValues[i]&&delete e[o]._visualValues[i]}}}const dh=t=>"reset"===t||"none"===t,hh=(t,e)=>e?t:Object.assign({},t);class uh{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=nh(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&ch(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),o=(t,e,i,o)=>"x"===t?e:"r"===t?o:i,n=e.xAxisID=dl(i.xAxisID,lh(t,"x")),a=e.yAxisID=dl(i.yAxisID,lh(t,"y")),s=e.rAxisID=dl(i.rAxisID,lh(t,"r")),r=e.indexAxis,l=e.iAxisID=o(r,n,a,s),c=e.vAxisID=o(r,a,n,s);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(a),e.rScale=this.getScaleForId(s),e.iScale=this.getScaleForId(l),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&ec(this._data,this),t._stacked&&ch(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(rl(e)){const t=this._cachedMeta;this._data=function(t,e){const{iScale:i,vScale:o}=e,n="x"===i.axis?"x":"y",a="x"===o.axis?"x":"y",s=Object.keys(t),r=new Array(s.length);let l,c,d;for(l=0,c=s.length;l{const i="_onData"+kl(e),o=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=o.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}(e,this),this._syncList=[],this._data=e}}addElements(){const t=this._cachedMeta;this._dataCheck(),this.datasetElementType&&(t.dataset=new this.datasetElementType)}buildOrUpdateElements(t){const e=this._cachedMeta,i=this.getDataset();let o=!1;this._dataCheck();const n=e._stacked;e._stacked=nh(e.vScale,e),e.stack!==i.stack&&(o=!0,ch(e),e.stack=i.stack),this._resyncElements(t),(o||n!==e._stacked)&&(rh(this,e._parsed),e._stacked=nh(e.vScale,e))}configure(){const t=this.chart.config,e=t.datasetScopeKeys(this._type),i=t.getOptionScopes(this.getDataset(),e,!0);this.options=t.createResolver(i,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(t,e){const{_cachedMeta:i,_data:o}=this,{iScale:n,_stacked:a}=i,s=n.axis;let r,l,c,d=0===t&&e===o.length||i._sorted,h=t>0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=o,i._sorted=!0,c=o;else{c=sl(o[t])?this.parseArrayData(i,o,t,e):rl(o[t])?this.parseObjectData(i,o,t,e):this.parsePrimitiveData(i,o,t,e);const n=()=>null===l[s]||h&&l[s]t&&!e.hidden&&e._stacked&&{keys:ih(i,!0),values:null})(e,i,this.chart),l={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:o,maxDefined:n}=t.getUserBounds();return{min:o?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(s);let h,u;function m(){u=o[h];const e=u[s.axis];return!ll(u[t.axis])||c>e||d=0;--h)if(!m()){this.updateRangeFromParsed(l,t,u,r);break}return l}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let o,n,a;for(o=0,n=e.length;o=0&&tthis.getContext(i,o,e)),d);return m.$shared&&(m.$shared=r,n[a]=Object.freeze(hh(m,r))),m}_resolveAnimations(t,e,i){const o=this.chart,n=this._cachedDataOpts,a=`animation-${e}`,s=n[a];if(s)return s;let r;if(!1!==o.options.animation){const o=this.chart.config,n=o.datasetAnimationScopeKeys(this._type,e),a=o.getOptionScopes(this.getDataset(),n);r=o.createResolver(a,this.getContext(t,i,e))}const l=new th(o,r&&r.animations);return r&&r._cacheable&&(n[a]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||dh(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),o=this._sharedOptions,n=this.getSharedOptions(i),a=this.includeOptions(e,n)||n!==o;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:a}}updateElement(t,e,i,o){dh(o)?Object.assign(t,i):this._resolveAnimations(e,o).update(t,i)}updateSharedOptions(t,e,i){t&&!dh(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,o){t.active=o;const n=this.getStyle(e,o);this._resolveAnimations(e,i,o).update(t,{options:!o&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const o=i.length,n=e.length,a=Math.min(n,o);a&&this.parse(0,a),n>o?this._insertElements(o,n-o,t):n{for(t.length+=e,s=t.length-1;s>=a;s--)t[s]=t[s-e]};for(r(n),s=t;st-e)))}return t._cache.$bar}(e,t.type);let o,n,a,s,r=e._length;const l=()=>{32767!==a&&-32768!==a&&(Tl(s)&&(r=Math.min(r,Math.abs(a-s)||r)),s=a)};for(o=0,n=i.length;oMath.abs(r)&&(l=r,c=s),e[i.axis]=c,e._custom={barStart:l,barEnd:c,start:n,end:a,min:s,max:r}}(t,e,i,o):e[i.axis]=i.parse(t,o),e}function _h(t,e,i,o){const n=t.iScale,a=t.vScale,s=n.getLabels(),r=n===a,l=[];let c,d,h,u;for(c=i,d=i+o;ct.x,i="left",o="right"):(e=t.baset.controller.options.grouped)),n=i.options.stacked,a=[],s=this._cachedMeta.controller.getParsed(e),r=s&&s[i.axis],l=t=>{const e=t._parsed.find((t=>t[i.axis]===r)),o=e&&e[t.vScale.axis];if(al(o)||isNaN(o))return!0};for(const i of o)if((void 0===e||!l(i))&&((!1===n||-1===a.indexOf(i.stack)||void 0===n&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getAxisCount(){return this._getAxis().length}getFirstScaleIdForIndexAxis(){const t=this.chart.scales,e=this.chart.options.indexAxis;return Object.keys(t).filter((i=>t[i].axis===e)).shift()}_getAxis(){const t={},e=this.getFirstScaleIdForIndexAxis();for(const i of this.chart.data.datasets)t[dl("x"===this.chart.options.indexAxis?i.xAxisID:i.yAxisID,e)]=!0;return Object.keys(t)}_getStackIndex(t,e,i){const o=this._getStacks(t,i),n=void 0!==e?o.indexOf(e):-1;return-1===n?o.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,o=[];let n,a;for(n=0,a=e.data.length;n=i?1:-1)}(h,e,s)*a,u===s&&(g-=h/2);const t=e.getPixelForDecimal(0),n=e.getPixelForDecimal(1),l=Math.min(t,n),m=Math.max(t,n);g=Math.max(Math.min(g,m),l),d=g+h,i&&!c&&(r._stacks[e.axis]._visualValues[o]=e.getValueForPixel(d)-e.getValueForPixel(g))}if(g===e.getPixelForValue(s)){const t=Vl(h)*e.getLineWidthForValue(s)/2;g+=t,h-=t}return{size:h,base:g,head:d,center:d+h/2}}_calculateBarIndexPixels(t,e){const i=e.scale,o=this.options,n=o.skipNull,a=dl(o.maxBarThickness,1/0);let s,r;const l=this._getAxisCount();if(e.grouped){const i=n?this._getStackCount(t):e.stackCount,c="flex"===o.barThickness?function(t,e,i,o){const n=e.pixels,a=n[t];let s=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:o,yScale:n}=e,a=this.getParsed(t),s=o.getLabelForValue(a.x),r=n.getLabelForValue(a.y),l=a._custom;return{label:i[t]||"",value:"("+s+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,o){const n="reset"===o,{iScale:a,vScale:s}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,o),c=a.axis,d=s.axis;for(let h=e;h"spacing"!==t,_indexable:t=>"spacing"!==t&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data,{labels:{pointStyle:i,textAlign:o,color:n,useBorderRadius:a,borderRadius:s}}=t.legend.options;return e.labels.length&&e.datasets.length?e.labels.map(((e,r)=>{const l=t.getDatasetMeta(0).controller.getStyle(r);return{text:e,fillStyle:l.backgroundColor,fontColor:n,hidden:!t.getDataVisibility(r),lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:l.borderWidth,strokeStyle:l.borderColor,textAlign:o,pointStyle:i,borderRadius:a&&(s||l.borderRadius),index:r}})):[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,o=this._cachedMeta;if(!1===this._parsing)o._parsed=i;else{let n,a,s=t=>+i[t];if(rl(i[t])){const{key:t="value"}=this._parsing;s=e=>+xl(i[e],t)}for(n=t,a=t+e;nZl(t,r,l,!0)?1:Math.max(e,e*i,o,o*i),p=(t,e,o)=>Zl(t,r,l,!0)?-1:Math.min(e,e*i,o,o*i),_=m(0,c,h),g=m(Ll,d,u),f=p(Dl,c,h),b=p(Dl+Ll,d,u);o=(_-f)/2,n=(g-b)/2,a=-(_+f)/2,s=-(g+b)/2}return{ratioX:o,ratioY:n,offsetX:a,offsetY:s}}(u,h,r),f=(i.width-a)/m,b=(i.height-a)/p,v=Math.max(Math.min(f,b)/2,0),y=hl(this.options.radius,v),w=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=_*y,this.offsetY=g*y,o.total=this.calculateTotal(),this.outerRadius=y-w*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-w*d,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,o=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===o._parsed[t]||o.data[t].hidden?0:this.calculateCircumference(o._parsed[t]*n/Cl)}updateElements(t,e,i,o){const n="reset"===o,a=this.chart,s=a.chartArea,r=a.options.animation,l=(s.left+s.right)/2,c=(s.top+s.bottom)/2,d=n&&r.animateScale,h=d?0:this.innerRadius,u=d?0:this.outerRadius,{sharedOptions:m,includeOptions:p}=this._getSharedOptions(e,o);let _,g=this._getRotation();for(_=0;_0&&!isNaN(t)?Cl*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,o=i.data.labels||[],n=vc(e._parsed[t],i.options.locale);return{label:o[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let o,n,a,s,r;if(!t)for(o=0,n=i.data.datasets.length;o0&&this.getParsed(e-1);for(let i=0;i=f){b.skip=!0;continue}const y=this.getParsed(i),w=al(y[u]),x=b[h]=a.getPixelForValue(y[h],i),k=b[u]=n||w?s.getBasePixel():s.getPixelForValue(r?this.applyStack(s,y,r):y[u],i);b.skip=isNaN(x)||isNaN(k)||w,b.stop=i>0&&Math.abs(y[h]-v[h])>_,p&&(b.parsed=y,b.raw=l.data[i]),d&&(b.options=c||this.resolveDataElementOptions(i,m.active?"active":o)),g||this.updateElement(m,i,b,o),v=y}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,o=t.data||[];if(!o.length)return i;const n=o[0].size(this.resolveDataElementOptions(0)),a=o[o.length-1].size(this.resolveDataElementOptions(o.length-1));return Math.max(i,n,a)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}}class Sh extends uh{static id="polarArea";static defaults={dataElementType:"arc",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:"number",properties:["x","y","startAngle","endAngle","innerRadius","outerRadius"]}},indexAxis:"r",startAngle:0};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i,color:o}}=t.legend.options;return e.labels.map(((e,n)=>{const a=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:a.backgroundColor,strokeStyle:a.borderColor,fontColor:o,lineWidth:a.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,o=i.data.labels||[],n=vc(e._parsed[t].r,i.options.locale);return{label:o[t]||"",value:n}}parseObjectData(t,e,i,o){return md.bind(this)(t,e,i,o)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const o=this.getParsed(i).r;!isNaN(o)&&this.chart.getDataVisibility(i)&&(oe.max&&(e.max=o))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,o=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(o/2,0),a=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-a*this.index,this.innerRadius=this.outerRadius-a}updateElements(t,e,i,o){const n="reset"===o,a=this.chart,s=a.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,c=r.yCenter,d=r.getIndexAngle(0)-.5*Dl;let h,u=d;const m=360/this.countVisibleElements();for(h=0;h{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?Fl(this.resolveDataElementOptions(t,e).angle||i):0}}class Mh extends kh{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}}class Dh extends uh{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,o){return md.bind(this)(t,e,i,o)}update(t){const e=this._cachedMeta,i=e.dataset,o=e.data||[],n=e.iScale.getLabels();if(i.points=o,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const a={_loop:!0,_fullLoop:n.length===o.length,options:e};this.updateElement(i,void 0,a,t)}this.updateElements(o,0,o.length,t)}updateElements(t,e,i,o){const n=this._cachedMeta.rScale,a="reset"===o;for(let s=e;s0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[u]-b[u])>g,_&&(p.parsed=i,p.raw=l.data[c]),h&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":o)),f||this.updateElement(e,c,p,o),b=i}this.updateSharedOptions(d,o,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,o=i.options&&i.options.borderWidth||0;if(!e.length)return o;const n=e[0].size(this.resolveDataElementOptions(0)),a=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(o,n,a)/2}}var Ah=Object.freeze({__proto__:null,BarController:wh,BubbleController:xh,DoughnutController:kh,LineController:Th,PieController:Mh,PolarAreaController:Sh,RadarController:Dh,ScatterController:Ch});function Ph(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class Eh{static override(t){Object.assign(Eh.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return Ph()}parse(){return Ph()}format(){return Ph()}add(){return Ph()}diff(){return Ph()}startOf(){return Ph()}endOf(){return Ph()}}var Lh={_date:Eh};function $h(t,e,i,o){const{controller:n,data:a,_sorted:s}=t,r=n._cachedMeta.iScale,l=t.dataset&&t.dataset.options?t.dataset.options.spanGaps:null;if(r&&e===r.axis&&"r"!==e&&s&&a.length){const s=r._reversePixels?Jl:Ql;if(!o){const o=s(a,e,i);if(l){const{vScale:e}=n._cachedMeta,{_parsed:i}=t,a=i.slice(0,o.lo+1).reverse().findIndex((t=>!al(t[e.axis])));o.lo-=Math.max(0,a);const s=i.slice(o.hi).findIndex((t=>!al(t[e.axis])));o.hi+=Math.max(0,s)}return o}if(n._sharedOptions){const t=a[0],o="function"==typeof t.getRange&&t.getRange(e);if(o){const t=s(a,e,i-o),n=s(a,e,i+o);return{lo:t.lo,hi:n.hi}}}}return{lo:0,hi:a.length-1}}function zh(t,e,i,o,n){const a=t.getSortedVisibleDatasetMetas(),s=i[e];for(let t=0,i=a.length;t{t[s]&&t[s](e[i],n)&&(a.push({element:t,datasetIndex:o,index:l}),r=r||t.inRange(e.x,e.y,n))})),o&&!r?[]:a}var Ih={evaluateInteractionItems:zh,modes:{index(t,e,i,o){const n=Cd(e,t),a=i.axis||"x",s=i.includeInvisible||!1,r=i.intersect?Oh(t,n,a,o,s):Hh(t,n,a,!1,o,s),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,o){const n=Cd(e,t),a=i.axis||"xy",s=i.includeInvisible||!1;let r=i.intersect?Oh(t,n,a,o,s):Hh(t,n,a,!1,o,s);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tOh(t,Cd(e,t),i.axis||"xy",o,i.includeInvisible||!1),nearest(t,e,i,o){const n=Cd(e,t),a=i.axis||"xy",s=i.includeInvisible||!1;return Hh(t,n,a,i.intersect,o,s)},x:(t,e,i,o)=>Rh(t,Cd(e,t),"x",i.intersect,o),y:(t,e,i,o)=>Rh(t,Cd(e,t),"y",i.intersect,o)}};const Nh=["left","top","right","bottom"];function Fh(t,e){return t.filter((t=>t.pos===e))}function Wh(t,e){return t.filter((t=>-1===Nh.indexOf(t.pos)&&t.box.axis===e))}function Bh(t,e){return t.sort(((t,i)=>{const o=e?i:t,n=e?t:i;return o.weight===n.weight?o.index-n.index:o.weight-n.weight}))}function jh(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:o,stackWeight:n}=i;if(!t||!Nh.includes(o))continue;const a=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});a.count++,a.weight+=n}return e}(t),{vBoxMaxWidth:o,hBoxMaxHeight:n}=e;let a,s,r;for(a=0,s=t.length;a{o[t]=Math.max(e[t],i[t])})),o}return o(t?["left","right"]:["top","bottom"])}function Xh(t,e,i,o){const n=[];let a,s,r,l,c,d;for(a=0,s=t.length,c=0;at.box.fullSize)),!0),o=Bh(Fh(e,"left"),!0),n=Bh(Fh(e,"right")),a=Bh(Fh(e,"top"),!0),s=Bh(Fh(e,"bottom")),r=Wh(e,"x"),l=Wh(e,"y");return{fullSize:i,leftAndTop:o.concat(a),rightAndBottom:n.concat(l).concat(s).concat(r),chartArea:Fh(e,"chartArea"),vertical:o.concat(n).concat(l),horizontal:a.concat(s).concat(r)}}(t.boxes),l=r.vertical,c=r.horizontal;ml(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const d=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,h=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:a,availableHeight:s,vBoxMaxWidth:a/2/d,hBoxMaxHeight:s/2}),u=Object.assign({},n);Yh(u,Kc(o));const m=Object.assign({maxPadding:u,w:a,h:s,x:n.left,y:n.top},n),p=jh(l.concat(c),h);Xh(r.fullSize,m,h,p),Xh(l,m,h,p),Xh(c,m,h,p)&&Xh(l,m,h,p),function(t){const e=t.maxPadding;function i(i){const o=Math.max(e[i]-t[i],0);return t[i]+=o,o}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(m),Gh(r.leftAndTop,m,h,p),m.x+=m.w,m.y+=m.h,Gh(r.rightAndBottom,m,h,p),t.chartArea={left:m.left,top:m.top,right:m.left+m.w,bottom:m.top+m.h,height:m.h,width:m.w},ml(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(m.w,m.h,{left:0,top:0,right:0,bottom:0})}))}};class Jh{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,o){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,o?Math.floor(e/o):i)}}isAttached(t){return!0}updateConfig(t){}}class tu extends Jh{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const eu="$chartjs",iu={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},ou=t=>null===t||""===t;const nu=!!Ld&&{passive:!0};function au(t,e,i){t&&t.canvas&&t.canvas.removeEventListener(e,i,nu)}function su(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function ru(t,e,i){const o=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||su(i.addedNodes,o),e=e&&!su(i.removedNodes,o);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function lu(t,e,i){const o=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||su(i.removedNodes,o),e=e&&!su(i.addedNodes,o);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const cu=new Map;let du=0;function hu(){const t=window.devicePixelRatio;t!==du&&(du=t,cu.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function uu(t,e,i){const o=t.canvas,n=o&&xd(o);if(!n)return;const a=nc(((t,e)=>{const o=n.clientWidth;i(t,e),o{const e=t[0],i=e.contentRect.width,o=e.contentRect.height;0===i&&0===o||a(i,o)}));return s.observe(n),function(t,e){cu.size||window.addEventListener("resize",hu),cu.set(t,e)}(t,a),s}function mu(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){cu.delete(t),cu.size||window.removeEventListener("resize",hu)}(t)}function pu(t,e,i){const o=t.canvas,n=nc((e=>{null!==t.ctx&&i(function(t,e){const i=iu[t.type]||t.type,{x:o,y:n}=Cd(t,e);return{type:i,chart:e,native:t,x:void 0!==o?o:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t&&t.addEventListener(e,i,nu)}(o,e,n),n}class _u extends Jh{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,o=t.getAttribute("height"),n=t.getAttribute("width");if(t[eu]={initial:{height:o,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",ou(n)){const e=$d(t,"width");void 0!==e&&(t.width=e)}if(ou(o))if(""===t.style.height)t.height=t.width/(e||2);else{const e=$d(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[eu])return!1;const i=e[eu].initial;["height","width"].forEach((t=>{const o=i[t];al(o)?e.removeAttribute(t):e.setAttribute(t,o)}));const o=i.style||{};return Object.keys(o).forEach((t=>{e.style[t]=o[t]})),e.width=e.width,delete e[eu],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const o=t.$proxies||(t.$proxies={}),n={attach:ru,detach:lu,resize:uu}[e]||pu;o[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),o=i[e];if(!o)return;({attach:mu,detach:mu,resize:mu}[e]||au)(t,e,o),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,o){return Pd(t,e,i,o)}isAttached(t){const e=t&&xd(t);return!(!e||!e.isConnected)}}function gu(t){return!wd()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?tu:_u}class fu{static defaults={};static defaultRoutes=void 0;x;y;active=!1;options;$animations;tooltipPosition(t){const{x:e,y:i}=this.getProps(["x","y"],t);return{x:e,y:i}}hasValue(){return Il(this.x)&&Il(this.y)}getProps(t,e){const i=this.$animations;if(!e||!i)return this;const o={};return t.forEach((t=>{o[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),o}}function bu(t,e){const i=t.options.ticks,o=function(t){const e=t.options.offset,i=t._tickSize(),o=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(o,n))}(t),n=Math.min(i.maxTicksLimit||o,o),a=i.major.enabled?function(t){const e=[];let i,o;for(i=0,o=t.length;in)return function(t,e,i,o){let n,a=0,s=i[0];for(o=Math.ceil(o),n=0;nt-e)).pop(),e}(o);for(let t=0,e=a.length-1;tn)return e}return Math.max(n,1)}(a,e,n);if(s>0){let t,i;const o=s>1?Math.round((l-r)/(s-1)):null;for(vu(e,c,d,al(o)?0:r-o,r),t=0,i=s-1;t"top"===e||"left"===e?t[e]+i:t[e]-i,wu=(t,e)=>Math.min(e||t,t);function xu(t,e){const i=[],o=t.length/e,n=t.length;let a=0;for(;as+r)))return c}function Tu(t){return t.drawTicks?t.tickLength:0}function Su(t,e){if(!t.display)return 0;const i=Gc(t.font,e),o=Kc(t.padding);return(sl(t.text)?t.text.length:1)*i.lineHeight+o.height}function Mu(t,e,i){let o=ac(t);return(i&&"right"!==e||!i&&"right"===e)&&(o=(t=>"left"===t?"right":"right"===t?"left":t)(o)),o}class Du extends fu{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:o}=this;return t=cl(t,Number.POSITIVE_INFINITY),e=cl(e,Number.NEGATIVE_INFINITY),i=cl(i,Number.POSITIVE_INFINITY),o=cl(o,Number.NEGATIVE_INFINITY),{min:cl(t,i),max:cl(e,o),minDefined:ll(t),maxDefined:ll(e)}}getMinMax(t){let e,{min:i,max:o,minDefined:n,maxDefined:a}=this.getUserBounds();if(n&&a)return{min:i,max:o};const s=this.getMatchingVisibleMetas();for(let r=0,l=s.length;ro?o:i,o=n&&i>o?i:o,{min:cl(i,cl(o,i)),max:cl(o,cl(i,o))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){ul(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:o,grace:n,ticks:a}=this.options,s=a.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=function(t,e,i){const{min:o,max:n}=t,a=hl(e,(n-o)/2),s=(t,e)=>i&&0===t?0:t+e;return{min:s(o,-Math.abs(a)),max:s(n,a)}}(this,n,o),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=s=n||i<=1||!this.isHorizontal())return void(this.labelRotation=o);const c=this._getLabelSizes(),d=c.widest.width,h=c.highest.height,u=Xl(this.chart.width-d,0,this.maxWidth);a=t.offset?this.maxWidth/i:u/(i-1),d+6>a&&(a=u/(i-(t.offset?.5:1)),s=this.maxHeight-Tu(t.grid)-e.padding-Su(t.title,this.chart.options.font),r=Math.sqrt(d*d+h*h),l=Wl(Math.min(Math.asin(Xl((c.highest.height+6)/a,-1,1)),Math.asin(Xl(s/r,-1,1))-Math.asin(Xl(h/r,-1,1)))),l=Math.max(o,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){ul(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){ul(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:o,grid:n}}=this,a=this._isVisible(),s=this.isHorizontal();if(a){const a=Su(o,e.options.font);if(s?(t.width=this.maxWidth,t.height=Tu(n)+a):(t.height=this.maxHeight,t.width=Tu(n)+a),i.display&&this.ticks.length){const{first:e,last:o,widest:n,highest:a}=this._getLabelSizes(),r=2*i.padding,l=Fl(this.labelRotation),c=Math.cos(l),d=Math.sin(l);if(s){const e=i.mirror?0:d*n.width+c*a.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:c*n.width+d*a.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,o,d,c)}}this._handleMargins(),s?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,o){const{ticks:{align:n,padding:a},position:s}=this.options,r=0!==this.labelRotation,l="top"!==s&&"x"===this.axis;if(this.isHorizontal()){const s=this.getPixelForTick(0)-this.left,c=this.right-this.getPixelForTick(this.ticks.length-1);let d=0,h=0;r?l?(d=o*t.width,h=i*e.height):(d=i*t.height,h=o*e.width):"start"===n?h=e.width:"end"===n?d=t.width:"inner"!==n&&(d=t.width/2,h=e.width/2),this.paddingLeft=Math.max((d-s+a)*this.width/(this.width-s),0),this.paddingRight=Math.max((h-c+a)*this.width/(this.width-c),0)}else{let i=e.height/2,o=t.height/2;"start"===n?(i=0,o=t.height):"end"===n&&(i=e.height,o=0),this.paddingTop=i+a,this.paddingBottom=o+a}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){ul(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,o=i.length/2;let n;if(o>e){for(n=0;n({width:a[t]||0,height:s[t]||0});return{first:k(0),last:k(e-1),widest:k(w),highest:k(x),widths:a,heights:s}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Xl(this._alignToPixels?Pc(this.chart,e,0):e,-32768,32767)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ts*o?s/i:r/o:r*o0}_computeGridLineItems(t){const e=this.axis,i=this.chart,o=this.options,{grid:n,position:a,border:s}=o,r=n.offset,l=this.isHorizontal(),c=this.ticks.length+(r?1:0),d=Tu(n),h=[],u=s.setContext(this.getContext()),m=u.display?u.width:0,p=m/2,_=function(t){return Pc(i,t,m)};let g,f,b,v,y,w,x,k,T,S,M,D;if("top"===a)g=_(this.bottom),w=this.bottom-d,k=g-p,S=_(t.top)+p,D=t.bottom;else if("bottom"===a)g=_(this.top),S=t.top,D=_(t.bottom)-p,w=g+p,k=this.top+d;else if("left"===a)g=_(this.right),y=this.right-d,x=g-p,T=_(t.left)+p,M=t.right;else if("right"===a)g=_(this.left),T=t.left,M=_(t.right)-p,y=g+p,x=this.left+d;else if("x"===e){if("center"===a)g=_((t.top+t.bottom)/2+.5);else if(rl(a)){const t=Object.keys(a)[0],e=a[t];g=_(this.chart.scales[t].getPixelForValue(e))}S=t.top,D=t.bottom,w=g+p,k=w+d}else if("y"===e){if("center"===a)g=_((t.left+t.right)/2);else if(rl(a)){const t=Object.keys(a)[0],e=a[t];g=_(this.chart.scales[t].getPixelForValue(e))}y=g-p,x=y-d,T=t.left,M=t.right}const C=dl(o.ticks.maxTicksLimit,c),A=Math.max(1,Math.ceil(c/C));for(f=0;f0&&(a-=o/2)}h={left:a,top:n,width:o+e.width,height:i+e.height,color:t.backdropColor}}_.push({label:v,font:T,textOffset:D,options:{rotation:p,color:i,strokeColor:r,strokeWidth:c,textAlign:u,textBaseline:C,translation:[y,w],backdrop:h}})}return _}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-Fl(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align?i="right":"inner"===e.align&&(i="inner"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:o,padding:n}}=this.options,a=t+n,s=this._getLabelSizes().widest.width;let r,l;return"left"===e?o?(l=this.right+n,"near"===i?r="left":"center"===i?(r="center",l+=s/2):(r="right",l+=s)):(l=this.right-a,"near"===i?r="right":"center"===i?(r="center",l-=s/2):(r="left",l=this.left)):"right"===e?o?(l=this.left+n,"near"===i?r="right":"center"===i?(r="center",l-=s/2):(r="left",l-=s)):(l=this.left+a,"near"===i?r="left":"center"===i?(r="center",l+=s/2):(r="right",l=this.right)):r="right",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return"left"===e||"right"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:"top"===e||"bottom"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:o,width:n,height:a}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,o,n,a),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,o=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,a;const s=(t,e,o)=>{o.width&&o.color&&(i.save(),i.lineWidth=o.width,i.strokeStyle=o.color,i.setLineDash(o.borderDash||[]),i.lineDashOffset=o.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,a=o.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:o,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",o=[];let n,a;for(n=0,a=e.length;n{const o=i.split("."),n=o.pop(),a=[t].concat(o).join("."),s=e[i].split("."),r=s.pop(),l=s.join(".");Dc.route(a,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&Dc.describe(e,t.descriptors)}(t,a,i),this.override&&Dc.override(t.id,t.overrides)),a}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,o=this.scope;i in e&&delete e[i],o&&i in Dc[o]&&(delete Dc[o][i],this.override&&delete xc[i])}}class Au{constructor(){this.controllers=new Cu(uh,"datasets",!0),this.elements=new Cu(fu,"elements"),this.plugins=new Cu(Object,"plugins"),this.scales=new Cu(Du,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const o=i||this._getRegistryForType(e);i||o.isForType(e)||o===this.plugins&&e.id?this._exec(t,o,e):ml(e,(e=>{const o=i||this._getRegistryForType(e);this._exec(t,o,e)}))}))}_exec(t,e,i){const o=kl(t);ul(i["before"+o],[],i),e[t](i),ul(i["after"+o],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(o(e,i),t,"stop"),this._notify(o(i,e),t,"start")}}function Lu(t,e){return e||!1!==t?!0===t?{}:t:null}function $u(t,{plugin:e,local:i},o,n){const a=t.pluginScopeKeys(e),s=t.getOptionScopes(o,a);return i&&e.defaults&&s.push(e.defaults),t.createResolver(s,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function zu(t,e){const i=Dc.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function Ou(t){if("x"===t||"y"===t||"r"===t)return t}function Vu(t,...e){if(Ou(t))return t;for(const o of e){const e=o.axis||("top"===(i=o.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&Ou(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function Hu(t,e,i){if(i[e+"AxisID"]===t)return{axis:e}}function Ru(t,e){const i=xc[t.type]||{scales:{}},o=e.scales||{},n=zu(t.type,e),a=Object.create(null);return Object.keys(o).forEach((e=>{const s=o[e];if(!rl(s))return console.error(`Invalid scale configuration for scale: ${e}`);if(s._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const r=Vu(e,s,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return Hu(t,"x",i[0])||Hu(t,"y",i[0])}return{}}(e,t),Dc.scales[s.type]),l=function(t,e){return t===e?"_index_":"_value_"}(r,n),c=i.scales||{};a[e]=vl(Object.create(null),[{axis:r},s,c[r],c[l]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,s=i.indexAxis||zu(n,e),r=(xc[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,s),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),vl(a[n],[{axis:e},o[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];vl(e,[Dc.scales[e.type],Dc.scale])})),a}function Iu(t){const e=t.options||(t.options={});e.plugins=dl(e.plugins,{}),e.scales=Ru(t,e)}function Nu(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const Fu=new Map,Wu=new Set;function Bu(t,e){let i=Fu.get(t);return i||(i=e(),Fu.set(t,i),Wu.add(i)),i}const ju=(t,e,i)=>{const o=xl(e,i);void 0!==o&&t.add(o)};class Uu{constructor(t){this._config=function(t){return(t=t||{}).data=Nu(t.data),Iu(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=Nu(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),Iu(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return Bu(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return Bu(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return Bu(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return Bu(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let o=i.get(t);return o&&!e||(o=new Map,i.set(t,o)),o}getOptionScopes(t,e,i){const{options:o,type:n}=this,a=this._cachedScopes(t,i),s=a.get(e);if(s)return s;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>ju(r,t,e)))),e.forEach((t=>ju(r,o,t))),e.forEach((t=>ju(r,xc[n]||{},t))),e.forEach((t=>ju(r,Dc,t))),e.forEach((t=>ju(r,kc,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),Wu.has(e)&&a.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,xc[e]||{},Dc.datasets[e]||{},{type:e},Dc,kc]}resolveNamedOptions(t,e,i,o=[""]){const n={$shared:!0},{resolver:a,subPrefixes:s}=Yu(this._resolverCache,t,o);let r=a;if(function(t,e){const{isScriptable:i,isIndexable:o}=id(t);for(const n of e){const e=i(n),a=o(n),s=(a||e)&&t[n];if(e&&(Sl(s)||qu(s))||a&&sl(s))return!0}return!1}(a,e)){n.$shared=!1;r=ed(a,i=Sl(i)?i():i,this.createResolver(t,i,s))}for(const t of e)n[t]=r[t];return n}createResolver(t,e,i=[""],o){const{resolver:n}=Yu(this._resolverCache,t,i);return rl(e)?ed(n,e,void 0,o):n}}function Yu(t,e,i){let o=t.get(e);o||(o=new Map,t.set(e,o));const n=i.join();let a=o.get(n);if(!a){a={resolver:td(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},o.set(n,a)}return a}const qu=t=>rl(t)&&Object.getOwnPropertyNames(t).some((e=>Sl(t[e])));const Zu=["top","bottom","left","right","chartArea"];function Xu(t,e){return"top"===t||"bottom"===t||-1===Zu.indexOf(t)&&"x"===e}function Ku(t,e){return function(i,o){return i[t]===o[t]?i[e]-o[e]:i[t]-o[t]}}function Gu(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),ul(i&&i.onComplete,[t],e)}function Qu(t){const e=t.chart,i=e.options.animation;ul(i&&i.onProgress,[t],e)}function Ju(t){return wd()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const tm={},em=t=>{const e=Ju(t);return Object.values(tm).filter((t=>t.canvas===e)).pop()};function im(t,e,i){const o=Object.keys(t);for(const n of o){const o=+n;if(o>=e){const a=t[n];delete t[n],(i>0||o>e)&&(t[o+i]=a)}}}class om{static defaults=Dc;static instances=tm;static overrides=xc;static registry=Pu;static version="4.5.1";static getChart=em;static register(...t){Pu.add(...t),nm()}static unregister(...t){Pu.remove(...t),nm()}constructor(t,e){const i=this.config=new Uu(e),o=Ju(t),n=em(o);if(n)throw new Error("Canvas is already in use. Chart with ID '"+n.id+"' must be destroyed before the canvas with ID '"+n.canvas.id+"' can be reused.");const a=i.createResolver(i.chartOptionScopes(),this.getContext());this.platform=new(i.platform||gu(o)),this.platform.updateConfig(i);const s=this.platform.acquireContext(o,a.aspectRatio),r=s&&s.canvas,l=r&&r.height,c=r&&r.width;this.id=nl(),this.ctx=s,this.canvas=r,this.width=c,this.height=l,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new Eu,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=function(t,e){let i;return function(...o){return e?(clearTimeout(i),i=setTimeout(t,e,o)):t.apply(this,o),e}}((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],tm[this.id]=this,s&&r?(Kd.listen(this,"complete",Gu),Kd.listen(this,"progress",Qu),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:o,_aspectRatio:n}=this;return al(t)?e&&n?n:o?i/o:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return Pu}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():Ed(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Ec(this.canvas,this.ctx),this}stop(){return Kd.stop(this),this}resize(t,e){Kd.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,o=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,a=this.platform.getMaximumSize(o,t,e,n),s=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=a.width,this.height=a.height,this._aspectRatio=this.aspectRatio,Ed(this,s,!0)&&(this.notifyPlugins("resize",{size:a}),ul(i.onResize,[this,a],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){ml(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,o=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],o=Vu(t,i),n="r"===o,a="x"===o;return{options:i,dposition:n?"chartArea":a?"bottom":"left",dtype:n?"radialLinear":a?"category":"linear"}})))),ml(n,(e=>{const n=e.options,a=n.id,s=Vu(a,n),r=dl(n.type,e.dtype);void 0!==n.position&&Xu(n.position,s)===Xu(e.dposition)||(n.position=e.dposition),o[a]=!0;let l=null;if(a in i&&i[a].type===r)l=i[a];else{l=new(Pu.getScale(r))({id:a,type:r,ctx:this.ctx,chart:this}),i[l.id]=l}l.init(n,t)})),ml(o,((t,e)=>{t||delete i[e]})),ml(i,(t=>{Qh.configure(this,t,t.options),Qh.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,o;for(this._removeUnreferencedMetasets(),i=0,o=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),o=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let a=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(Ku("z","_idx"));const{_active:s,_lastEvent:r}=this;r?this._eventHandler(r,!0):s.length&&this._updateHoverStyles(s,s,!0),this.render()}_updateScales(){ml(this.scales,(t=>{Qh.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);Ml(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:o,count:n}of e){im(t,o,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),o=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;Qh.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],ml(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i={meta:t,index:t.index,cancelable:!0},o=Zd(this,t);!1!==this.notifyPlugins("beforeDatasetDraw",i)&&(o&&Oc(e,o),t.controller.draw(),o&&Vc(e),i.cancelable=!1,this.notifyPlugins("afterDatasetDraw",i))}isPointInArea(t){return zc(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,o){const n=Ih.modes[e];return"function"==typeof n?n(this,t,i,o):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let o=i.filter((t=>t&&t._dataset===e)).pop();return o||(o={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(o)),o}getContext(){return this.$context||(this.$context=Jc(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const o=i?"show":"hide",n=this.getDatasetMeta(t),a=n.controller._resolveAnimations(void 0,o);Tl(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),a.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?o:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),Kd.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,o),t[i]=o},o=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};ml(this.options.events,(t=>i(t,o)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,o)=>{e.addEventListener(this,i,o),t[i]=o},o=(i,o)=>{t[i]&&(e.removeEventListener(this,i,o),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let a;const s=()=>{o("attach",s),this.attached=!0,this.resize(),i("resize",n),i("detach",a)};a=()=>{this.attached=!1,o("resize",n),this._stop(),this._resize(0,0),i("attach",s)},e.isAttached(this.canvas)?s():a()}unbindEvents(){ml(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},ml(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const o=i?"set":"remove";let n,a,s,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+o+"DatasetHoverStyle"]()),s=0,r=t.length;s{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!pl(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const o=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),a=n(e,t),s=i?t:n(t,e);a.length&&this.updateHoverStyle(a,o.mode,!1),s.length&&o.mode&&this.updateHoverStyle(s,o.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},o=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,o))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,o),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:o=[],options:n}=this,a=e,s=this._getActiveElements(t,o,i,a),r=function(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}(t),l=function(t,e,i,o){return i&&"mouseout"!==t.type?o?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,ul(n.onHover,[t,s,this],this),r&&ul(n.onClick,[t,s,this],this));const c=!pl(s,o);return(c||e)&&(this._active=s,this._updateHoverStyles(s,o,e)),this._lastEvent=l,c}_getActiveElements(t,e,i,o){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,o)}}function nm(){return ml(om.instances,(t=>t._plugins.invalidate()))}function am(t,e,i,o){const n=qc(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const a=(i-e)/2,s=Math.min(a,o*e/2),r=t=>{const e=(i-Math.min(a,t))*o/2;return Xl(t,0,Math.min(a,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:Xl(n.innerStart,0,s),innerEnd:Xl(n.innerEnd,0,s)}}function sm(t,e,i,o){return{x:i+t*Math.cos(e),y:o+t*Math.sin(e)}}function rm(t,e,i,o,n,a){const{x:s,y:r,startAngle:l,pixelMargin:c,innerRadius:d}=e,h=Math.max(e.outerRadius+o+i-c,0),u=d>0?d+o+i+c:0;let m=0;const p=n-l;if(o){const t=((d>0?d-o:0)+(h>0?h-o:0))/2;m=(p-(0!==t?p*t/(t+o):p))/2}const _=(p-Math.max(.001,p*h-i/Dl)/h)/2,g=l+_+m,f=n-_-m,{outerStart:b,outerEnd:v,innerStart:y,innerEnd:w}=am(e,u,h,f-g),x=h-b,k=h-v,T=g+b/x,S=f-v/k,M=u+y,D=u+w,C=g+y/M,A=f-w/D;if(t.beginPath(),a){const e=(T+S)/2;if(t.arc(s,r,h,T,e),t.arc(s,r,h,e,S),v>0){const e=sm(k,S,s,r);t.arc(e.x,e.y,v,S,f+Ll)}const i=sm(D,f,s,r);if(t.lineTo(i.x,i.y),w>0){const e=sm(D,A,s,r);t.arc(e.x,e.y,w,f+Ll,A+Math.PI)}const o=(f-w/u+(g+y/u))/2;if(t.arc(s,r,u,f-w/u,o,!0),t.arc(s,r,u,o,g+y/u,!0),y>0){const e=sm(M,C,s,r);t.arc(e.x,e.y,y,C+Math.PI,g-Ll)}const n=sm(x,g,s,r);if(t.lineTo(n.x,n.y),b>0){const e=sm(x,T,s,r);t.arc(e.x,e.y,b,g-Ll,T)}}else{t.moveTo(s,r);const e=Math.cos(T)*h+s,i=Math.sin(T)*h+r;t.lineTo(e,i);const o=Math.cos(S)*h+s,n=Math.sin(S)*h+r;t.lineTo(o,n)}t.closePath()}function lm(t,e,i,o,n){const{fullCircles:a,startAngle:s,circumference:r,options:l}=e,{borderWidth:c,borderJoinStyle:d,borderDash:h,borderDashOffset:u,borderRadius:m}=l,p="inner"===l.borderAlign;if(!c)return;t.setLineDash(h||[]),t.lineDashOffset=u,p?(t.lineWidth=2*c,t.lineJoin=d||"round"):(t.lineWidth=c,t.lineJoin=d||"bevel");let _=e.endAngle;if(a){rm(t,e,i,o,_,n);for(let e=0;en?(c=n/l,t.arc(a,s,l,i+c,o-c,!0)):t.arc(a,s,n,i+Ll,o-Ll),t.closePath(),t.clip()}(t,e,_),l.selfJoin&&_-s>=Dl&&0===m&&"miter"!==d&&function(t,e,i){const{startAngle:o,x:n,y:a,outerRadius:s,innerRadius:r,options:l}=e,{borderWidth:c,borderJoinStyle:d}=l,h=Math.min(c/s,ql(o-i));if(t.beginPath(),t.arc(n,a,s-c/2,o+h/2,i-h/2),r>0){const e=Math.min(c/r,ql(o-i));t.arc(n,a,r+c/2,i-e/2,o+e/2,!0)}else{const e=Math.min(c/2,s*ql(o-i));if("round"===d)t.arc(n,a,e,i-Dl/2,o+Dl/2,!0);else if("bevel"===d){const s=2*e*e,r=-s*Math.cos(i+Dl/2)+n,l=-s*Math.sin(i+Dl/2)+a,c=s*Math.cos(o+Dl/2)+n,d=s*Math.sin(o+Dl/2)+a;t.lineTo(r,l),t.lineTo(c,d)}}t.closePath(),t.moveTo(0,0),t.rect(0,0,t.canvas.width,t.canvas.height),t.clip("evenodd")}(t,e,_),a||(rm(t,e,i,o,_,n),t.stroke())}class cm extends fu{static id="arc";static defaults={borderAlign:"center",borderColor:"#fff",borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0,circular:!0,selfJoin:!1};static defaultRoutes={backgroundColor:"backgroundColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const o=this.getProps(["x","y"],i),{angle:n,distance:a}=jl(o,{x:t,y:e}),{startAngle:s,endAngle:r,innerRadius:l,outerRadius:c,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),h=(this.options.spacing+this.options.borderWidth)/2,u=dl(d,r-s),m=Zl(n,s,r)&&s!==r,p=u>=Cl||m,_=Kl(a,l+h,c+h);return p&&_}getCenterPoint(t){const{x:e,y:i,startAngle:o,endAngle:n,innerRadius:a,outerRadius:s}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:r,spacing:l}=this.options,c=(o+n)/2,d=(a+s+l+r)/2;return{x:e+Math.cos(c)*d,y:i+Math.sin(c)*d}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,o=(e.offset||0)/4,n=(e.spacing||0)/2,a=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>Cl?Math.floor(i/Cl):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const s=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(s)*o,Math.sin(s)*o);const r=o*(1-Math.sin(Math.min(Dl,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,o,n){const{fullCircles:a,startAngle:s,circumference:r}=e;let l=e.endAngle;if(a){rm(t,e,i,o,l,n);for(let e=0;er&&a>r;return{count:o,start:l,loop:e.loop,ilen:c(s+(c?r-t:t))%a,v=()=>{m!==p&&(t.lineTo(g,p),t.lineTo(g,m),t.lineTo(g,_))};for(l&&(h=n[b(0)],t.moveTo(h.x,h.y)),d=0;d<=r;++d){if(h=n[b(d)],h.skip)continue;const e=h.x,i=h.y,o=0|e;o===u?(ip&&(p=i),g=(f*g+e)/++f):(v(),t.lineTo(e,i),u=o,f=0,m=p=i),_=i}v()}function _m(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?pm:mm}const gm="function"==typeof Path2D;function fm(t,e,i,o){gm&&!e.options.segment?function(t,e,i,o){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,o)&&n.closePath()),dm(t,e.options),t.stroke(n)}(t,e,i,o):function(t,e,i,o){const{segments:n,options:a}=e,s=_m(e);for(const r of n)dm(t,a,r.style),t.beginPath(),s(t,e,r,{start:i,end:i+o-1})&&t.closePath(),t.stroke()}(t,e,i,o)}class bm extends fu{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const o=i.spanGaps?this._loop:this._fullLoop;yd(this._points,i,t,o,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=function(t,e){const i=t.points,o=t.options.spanGaps,n=i.length;if(!n)return[];const a=!!t._loop,{start:s,end:r}=function(t,e,i,o){let n=0,a=e-1;if(i&&!o)for(;nn&&t[a%e].skip;)a--;return a%=e,{start:n,end:a}}(i,n,a,o);return jd(t,!0===o?[{start:s,end:r,loop:a}]:function(t,e,i,o){const n=t.length,a=[];let s,r=e,l=t[e];for(s=e+1;s<=i;++s){const i=t[s%n];i.skip||i.stop?l.skip||(o=!1,a.push({start:e%n,end:(s-1)%n,loop:o}),e=r=i.stop?s:null):(r=s,l.skip&&(e=s)),l=i}return null!==r&&a.push({start:e%n,end:r%n,loop:o}),a}(i,s,rt.replace("rgb(","rgba(").replace(")",", 0.5)")));function Em(t){return Am[t%Am.length]}function Lm(t){return Pm[t%Pm.length]}function $m(t){let e=0;return(i,o)=>{const n=t.getDatasetMeta(o).controller;n instanceof kh?e=function(t,e){return t.backgroundColor=t.data.map((()=>Em(e++))),e}(i,e):n instanceof Sh?e=function(t,e){return t.backgroundColor=t.data.map((()=>Lm(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Em(e),t.backgroundColor=Lm(e),++e}(i,e))}}function zm(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Om={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:o},options:n}=t.config,{elements:a}=n,s=zm(o)||(r=n)&&(r.borderColor||r.backgroundColor)||a&&zm(a)||"rgba(0,0,0,0.1)"!==Dc.borderColor||"rgba(0,0,0,0.1)"!==Dc.backgroundColor;var r;if(!i.forceOverride&&s)return;const l=$m(t);o.forEach(l)}};function Vm(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Hm(t){t.data.datasets.forEach((t=>{Vm(t)}))}var Rm={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Hm(t);const o=t.width;t.data.datasets.forEach(((e,n)=>{const{_data:a,indexAxis:s}=e,r=t.getDatasetMeta(n),l=a||e.data;if("y"===Qc([s,t.options.indexAxis]))return;if(!r.controller.supportsDecimation)return;const c=t.scales[r.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:h}=function(t,e){const i=e.length;let o,n=0;const{iScale:a}=t,{min:s,max:r,minDefined:l,maxDefined:c}=a.getUserBounds();return l&&(n=Xl(Ql(e,a.axis,s).lo,0,i-1)),o=c?Xl(Ql(e,a.axis,r).hi+1,n,i)-n:i-n,{start:n,count:o}}(r,l);if(h<=(i.threshold||4*o))return void Vm(e);let u;switch(al(a)&&(e._data=l,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":u=function(t,e,i,o,n){const a=n.samples||o;if(a>=i)return t.slice(e,e+i);const s=[],r=(i-2)/(a-2);let l=0;const c=e+i-1;let d,h,u,m,p,_=e;for(s[l++]=t[_],d=0;du&&(u=m,h=t[o],p=o);s[l++]=h,_=p}return s[l++]=t[c],s}(l,d,h,o,i);break;case"min-max":u=function(t,e,i,o){let n,a,s,r,l,c,d,h,u,m,p=0,_=0;const g=[],f=e+i-1,b=t[e].x,v=t[f].x-b;for(n=e;nm&&(m=r,d=n),p=(_*p+a.x)/++_;else{const i=n-1;if(!al(c)&&!al(d)){const e=Math.min(c,d),o=Math.max(c,d);e!==h&&e!==i&&g.push({...t[e],x:p}),o!==h&&o!==i&&g.push({...t[o],x:p})}n>0&&i!==h&&g.push(t[i]),g.push(a),l=e,_=0,u=m=r,c=d=h=n}}return g}(l,d,h,o);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=u}))},destroy(t){Hm(t)}};function Im(t,e,i,o){if(o)return;let n=e[t],a=i[t];return"angle"===t&&(n=ql(n),a=ql(a)),{property:t,start:n,end:a}}function Nm(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function Fm(t,e,i,o){return t&&e?o(t[i],e[i]):t?t[i]:e?e[i]:0}function Wm(t,e){let i=[],o=!1;return sl(t)?(o=!0,i=t):i=function(t,e){const{x:i=null,y:o=null}=t||{},n=e.points,a=[];return e.segments.forEach((({start:t,end:e})=>{e=Nm(t,e,n);const s=n[t],r=n[e];null!==o?(a.push({x:s.x,y:o}),a.push({x:r.x,y:o})):null!==i&&(a.push({x:i,y:s.y}),a.push({x:i,y:r.y}))})),a}(t,e),i.length?new bm({points:i,options:{tension:0},_loop:o,_fullLoop:o}):null}function Bm(t){return t&&!1!==t.fill}function jm(t,e,i){let o=t[e].fill;const n=[e];let a;if(!i)return o;for(;!1!==o&&-1===n.indexOf(o);){if(!ll(o))return o;if(a=t[o],!a)return!1;if(a.visible)return o;n.push(o),o=a.fill}return!1}function Um(t,e,i){const o=function(t){const e=t.options,i=e.fill;let o=dl(i&&i.target,i);void 0===o&&(o=!!e.backgroundColor);if(!1===o||null===o)return!1;if(!0===o)return"origin";return o}(t);if(rl(o))return!isNaN(o.value)&&o;let n=parseFloat(o);return ll(n)&&Math.floor(n)===n?function(t,e,i,o){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=o)return!1;return i}(o[0],e,n,i):["origin","start","end","stack","shape"].indexOf(o)>=0&&o}function Ym(t,e,i){const o=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(a,i.axis),o&&i.fill&&Km(t.ctx,i,a))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const o=t.getSortedVisibleDatasetMetas();for(let e=o.length-1;e>=0;--e){const i=o[e].$filler;Bm(i)&&Km(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const o=e.meta.$filler;Bm(o)&&"beforeDatasetDraw"===i.drawTime&&Km(t.ctx,o,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const op=(t,e)=>{let{boxHeight:i=e,boxWidth:o=e}=t;return t.usePointStyle&&(i=Math.min(i,e),o=t.pointStyleWidth||Math.min(o,e)),{boxWidth:o,boxHeight:i,itemHeight:Math.max(e,i)}};class np extends fu{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=ul(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,o=Gc(i.font),n=o.size,a=this._computeTitleHeight(),{boxWidth:s,itemHeight:r}=op(i,n);let l,c;e.font=o.string,this.isHorizontal()?(l=this.maxWidth,c=this._fitRows(a,n,s,r)+10):(c=this.maxHeight,l=this._fitCols(a,o,s,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(c,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,o){const{ctx:n,maxWidth:a,options:{labels:{padding:s}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],c=o+s;let d=t;n.textAlign="left",n.textBaseline="middle";let h=-1,u=-c;return this.legendItems.forEach(((t,m)=>{const p=i+e/2+n.measureText(t.text).width;(0===m||l[l.length-1]+p+2*s>a)&&(d+=c,l[l.length-(m>0?0:1)]=0,u+=c,h++),r[m]={left:0,top:u,row:h,width:p,height:o},l[l.length-1]+=p+s})),d}_fitCols(t,e,i,o){const{ctx:n,maxHeight:a,options:{labels:{padding:s}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],c=a-t;let d=s,h=0,u=0,m=0,p=0;return this.legendItems.forEach(((t,a)=>{const{itemWidth:_,itemHeight:g}=function(t,e,i,o,n){const a=function(t,e,i,o){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+o.measureText(n).width}(o,t,e,i),s=function(t,e,i){let o=t;"string"!=typeof e.text&&(o=ap(e,i));return o}(n,o,e.lineHeight);return{itemWidth:a,itemHeight:s}}(i,e,n,t,o);a>0&&u+g+2*s>c&&(d+=h+s,l.push({width:h,height:u}),m+=h+s,p++,h=u=0),r[a]={left:m,top:u,col:p,width:_,height:g},h=Math.max(h,_),u+=g+s})),d+=h,l.push({width:h,height:u}),d}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:o},rtl:n}}=this,a=Hd(n,this.left,this.width);if(this.isHorizontal()){let n=0,s=sc(i,this.left+o,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,s=sc(i,this.left+o,this.right-this.lineWidths[n])),r.top+=this.top+t+o,r.left=a.leftForLtr(a.x(s),r.width),s+=r.width+o}else{let n=0,s=sc(i,this.top+t+o,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,s=sc(i,this.top+t+o,this.bottom-this.columnSizes[n].height)),r.top=s,r.left+=this.left+o,r.left=a.leftForLtr(a.x(r.left),r.width),s+=r.height+o}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Oc(t,this),this._draw(),Vc(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:o}=this,{align:n,labels:a}=t,s=Dc.color,r=Hd(t.rtl,this.left,this.width),l=Gc(a.font),{padding:c}=a,d=l.size,h=d/2;let u;this.drawTitle(),o.textAlign=r.textAlign("left"),o.textBaseline="middle",o.lineWidth=.5,o.font=l.string;const{boxWidth:m,boxHeight:p,itemHeight:_}=op(a,d),g=this.isHorizontal(),f=this._computeTitleHeight();u=g?{x:sc(n,this.left+c,this.right-i[0]),y:this.top+c+f,line:0}:{x:this.left+c,y:sc(n,this.top+f+c,this.bottom-e[0].height),line:0},Rd(this.ctx,t.textDirection);const b=_+c;this.legendItems.forEach(((v,y)=>{o.strokeStyle=v.fontColor,o.fillStyle=v.fontColor;const w=o.measureText(v.text).width,x=r.textAlign(v.textAlign||(v.textAlign=a.textAlign)),k=m+h+w;let T=u.x,S=u.y;r.setWidth(this.width),g?y>0&&T+k+c>this.right&&(S=u.y+=b,u.line++,T=u.x=sc(n,this.left+c,this.right-i[u.line])):y>0&&S+b>this.bottom&&(T=u.x=T+e[u.line].width+c,u.line++,S=u.y=sc(n,this.top+f+c,this.bottom-e[u.line].height));if(function(t,e,i){if(isNaN(m)||m<=0||isNaN(p)||p<0)return;o.save();const n=dl(i.lineWidth,1);if(o.fillStyle=dl(i.fillStyle,s),o.lineCap=dl(i.lineCap,"butt"),o.lineDashOffset=dl(i.lineDashOffset,0),o.lineJoin=dl(i.lineJoin,"miter"),o.lineWidth=n,o.strokeStyle=dl(i.strokeStyle,s),o.setLineDash(dl(i.lineDash,[])),a.usePointStyle){const s={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,m/2);$c(o,s,l,e+h,a.pointStyleWidth&&m)}else{const a=e+Math.max((d-p)/2,0),s=r.leftForLtr(t,m),l=Xc(i.borderRadius);o.beginPath(),Object.values(l).some((t=>0!==t))?Wc(o,{x:s,y:a,w:m,h:p,radius:l}):o.rect(s,a,m,p),o.fill(),0!==n&&o.stroke()}o.restore()}(r.x(T),S,v),T=((t,e,i,o)=>t===(o?"left":"right")?i:"center"===t?(e+i)/2:e)(x,T+m+h,g?T+k:this.right,t.rtl),function(t,e,i){Fc(o,i.text,t,e+_/2,l,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(T),S,v),g)u.x+=k+c;else if("string"!=typeof v.text){const t=l.lineHeight;u.y+=ap(v,t)+c}else u.y+=b})),Id(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Gc(e.font),o=Kc(e.padding);if(!e.display)return;const n=Hd(t.rtl,this.left,this.width),a=this.ctx,s=e.position,r=i.size/2,l=o.top+r;let c,d=this.left,h=this.width;if(this.isHorizontal())h=Math.max(...this.lineWidths),c=this.top+l,d=sc(t.align,d,this.right-h);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);c=l+sc(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=sc(s,d,d+h);a.textAlign=n.textAlign(ac(s)),a.textBaseline="middle",a.strokeStyle=e.color,a.fillStyle=e.color,a.font=i.string,Fc(a,e.text,u,c,i)}_computeTitleHeight(){const t=this.options.title,e=Gc(t.font),i=Kc(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,o,n;if(Kl(t,this.left,this.right)&&Kl(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;inull!==t&&null!==e&&t.datasetIndex===e.datasetIndex&&t.index===e.index)(o,i);o&&!n&&ul(e.onLeave,[t,o,this],this),this._hoveredItem=i,i&&!n&&ul(e.onHover,[t,i,this],this)}else i&&ul(e.onClick,[t,i,this],this)}}function ap(t,e){return e*(t.text?t.text.length:0)}var sp={id:"legend",_element:np,start(t,e,i){const o=t.legend=new np({ctx:t.ctx,options:i,chart:t});Qh.configure(t,o,i),Qh.addBox(t,o)},stop(t){Qh.removeBox(t,t.legend),delete t.legend},beforeUpdate(t,e,i){const o=t.legend;Qh.configure(t,o,i),o.options=i},afterUpdate(t){const e=t.legend;e.buildLabels(),e.adjustHitBoxes()},afterEvent(t,e){e.replay||t.legend.handleEvent(e.event)},defaults:{display:!0,position:"top",align:"center",fullSize:!0,reverse:!1,weight:1e3,onClick(t,e,i){const o=e.datasetIndex,n=i.chart;n.isDatasetVisible(o)?(n.hide(o),e.hidden=!0):(n.show(o),e.hidden=!1)},onHover:null,onLeave:null,labels:{color:t=>t.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:o,textAlign:n,color:a,useBorderRadius:s,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),c=Kc(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:a,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(c.width+c.height)/4,strokeStyle:l.borderColor,pointStyle:o||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:s&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class rp extends fu{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const o=sl(i.text)?i.text.length:1;this._padding=Kc(i.padding);const n=o*Gc(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=n:this.width=n}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:o,right:n,options:a}=this,s=a.align;let r,l,c,d=0;return this.isHorizontal()?(l=sc(s,i,n),c=e+t,r=n-i):("left"===a.position?(l=i+t,c=sc(s,o,e),d=-.5*Dl):(l=n-t,c=sc(s,e,o),d=.5*Dl),r=o-e),{titleX:l,titleY:c,maxWidth:r,rotation:d}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Gc(e.font),o=i.lineHeight/2+this._padding.top,{titleX:n,titleY:a,maxWidth:s,rotation:r}=this._drawArgs(o);Fc(t,e.text,0,0,i,{color:e.color,maxWidth:s,rotation:r,textAlign:ac(e.align),textBaseline:"middle",translation:[n,a]})}}var lp={id:"title",_element:rp,start(t,e,i){!function(t,e){const i=new rp({ctx:t.ctx,options:e,chart:t});Qh.configure(t,i,e),Qh.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;Qh.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const o=t.titleBlock;Qh.configure(t,o,i),o.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const cp=new WeakMap;var dp={id:"subtitle",start(t,e,i){const o=new rp({ctx:t.ctx,options:i,chart:t});Qh.configure(t,o,i),Qh.addBox(t,o),cp.set(t,o)},stop(t){Qh.removeBox(t,cp.get(t)),cp.delete(t)},beforeUpdate(t,e,i){const o=cp.get(t);Qh.configure(t,o,i),o.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const hp={average(t){if(!t.length)return!1;let e,i,o=new Set,n=0,a=0;for(e=0,i=t.length;et+e))/o.size;return{x:s,y:n/a}},nearest(t,e){if(!t.length)return!1;let i,o,n,a=e.x,s=e.y,r=Number.POSITIVE_INFINITY;for(i=0,o=t.length;i-1?t.split("\n"):t}function pp(t,e){const{element:i,datasetIndex:o,index:n}=e,a=t.getDatasetMeta(o).controller,{label:s,value:r}=a.getLabelAndValue(n);return{chart:t,label:s,parsed:a.getParsed(n),raw:t.data.datasets[o].data[n],formattedValue:r,dataset:a.getDataset(),dataIndex:n,datasetIndex:o,element:i}}function _p(t,e){const i=t.chart.ctx,{body:o,footer:n,title:a}=t,{boxWidth:s,boxHeight:r}=e,l=Gc(e.bodyFont),c=Gc(e.titleFont),d=Gc(e.footerFont),h=a.length,u=n.length,m=o.length,p=Kc(e.padding);let _=p.height,g=0,f=o.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(f+=t.beforeBody.length+t.afterBody.length,h&&(_+=h*c.lineHeight+(h-1)*e.titleSpacing+e.titleMarginBottom),f){_+=m*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(f-m)*l.lineHeight+(f-1)*e.bodySpacing}u&&(_+=e.footerMarginTop+u*d.lineHeight+(u-1)*e.footerSpacing);let b=0;const v=function(t){g=Math.max(g,i.measureText(t).width+b)};return i.save(),i.font=c.string,ml(t.title,v),i.font=l.string,ml(t.beforeBody.concat(t.afterBody),v),b=e.displayColors?s+2+e.boxPadding:0,ml(o,(t=>{ml(t.before,v),ml(t.lines,v),ml(t.after,v)})),b=0,i.font=d.string,ml(t.footer,v),i.restore(),g+=p.width,{width:g,height:_}}function gp(t,e,i,o){const{x:n,width:a}=i,{width:s,chartArea:{left:r,right:l}}=t;let c="center";return"center"===o?c=n<=(r+l)/2?"left":"right":n<=a/2?c="left":n>=s-a/2&&(c="right"),function(t,e,i,o){const{x:n,width:a}=o,s=i.caretSize+i.caretPadding;return"left"===t&&n+a+s>e.width||"right"===t&&n-a-s<0||void 0}(c,t,e,i)&&(c="center"),c}function fp(t,e,i){const o=i.yAlign||e.yAlign||function(t,e){const{y:i,height:o}=e;return it.height-o/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||gp(t,e,i,o),yAlign:o}}function bp(t,e,i,o){const{caretSize:n,caretPadding:a,cornerRadius:s}=t,{xAlign:r,yAlign:l}=i,c=n+a,{topLeft:d,topRight:h,bottomLeft:u,bottomRight:m}=Xc(s);let p=function(t,e){let{x:i,width:o}=t;return"right"===e?i-=o:"center"===e&&(i-=o/2),i}(e,r);const _=function(t,e,i){let{y:o,height:n}=t;return"top"===e?o+=i:o-="bottom"===e?n+i:n/2,o}(e,l,c);return"center"===l?"left"===r?p+=c:"right"===r&&(p-=c):"left"===r?p-=Math.max(d,u)+n:"right"===r&&(p+=Math.max(h,m)+n),{x:Xl(p,0,o.width-e.width),y:Xl(_,0,o.height-e.height)}}function vp(t,e,i){const o=Kc(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-o.right:t.x+o.left}function yp(t){return up([],mp(t))}function wp(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const xp={beforeTitle:ol,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,o=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(o>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=wp(i,t);up(e.before,mp(kp(n,"beforeLabel",this,t))),up(e.lines,kp(n,"label",this,t)),up(e.after,mp(kp(n,"afterLabel",this,t))),o.push(e)})),o}getAfterBody(t,e){return yp(kp(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,o=kp(i,"beforeFooter",this,t),n=kp(i,"footer",this,t),a=kp(i,"afterFooter",this,t);let s=[];return s=up(s,mp(o)),s=up(s,mp(n)),s=up(s,mp(a)),s}_createItems(t){const e=this._active,i=this.chart.data,o=[],n=[],a=[];let s,r,l=[];for(s=0,r=e.length;st.filter(e,o,n,i)))),t.itemSort&&(l=l.sort(((e,o)=>t.itemSort(e,o,i)))),ml(l,(e=>{const i=wp(t.callbacks,e);o.push(kp(i,"labelColor",this,e)),n.push(kp(i,"labelPointStyle",this,e)),a.push(kp(i,"labelTextColor",this,e))})),this.labelColors=o,this.labelPointStyles=n,this.labelTextColors=a,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),o=this._active;let n,a=[];if(o.length){const t=hp[i.position].call(this,o,this._eventPosition);a=this._createItems(i),this.title=this.getTitle(a,i),this.beforeBody=this.getBeforeBody(a,i),this.body=this.getBody(a,i),this.afterBody=this.getAfterBody(a,i),this.footer=this.getFooter(a,i);const e=this._size=_p(this,i),s=Object.assign({},t,e),r=fp(this.chart,i,s),l=bp(i,s,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=a,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,o){const n=this.getCaretPosition(t,i,o);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:o,yAlign:n}=this,{caretSize:a,cornerRadius:s}=i,{topLeft:r,topRight:l,bottomLeft:c,bottomRight:d}=Xc(s),{x:h,y:u}=t,{width:m,height:p}=e;let _,g,f,b,v,y;return"center"===n?(v=u+p/2,"left"===o?(_=h,g=_-a,b=v+a,y=v-a):(_=h+m,g=_+a,b=v-a,y=v+a),f=_):(g="left"===o?h+Math.max(r,c)+a:"right"===o?h+m-Math.max(l,d)-a:this.caretX,"top"===n?(b=u,v=b-a,_=g-a,f=g+a):(b=u+p,v=b+a,_=g+a,f=g-a),y=b),{x1:_,x2:g,x3:f,y1:b,y2:v,y3:y}}drawTitle(t,e,i){const o=this.title,n=o.length;let a,s,r;if(n){const l=Hd(i.rtl,this.x,this.width);for(t.x=vp(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",a=Gc(i.titleFont),s=i.titleSpacing,e.fillStyle=i.titleColor,e.font=a.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,Wc(t,{x:e,y:m,w:l,h:r,radius:s}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),Wc(t,{x:i,y:m+1,w:l-2,h:r-2,radius:s}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,m,l,r),t.strokeRect(e,m,l,r),t.fillStyle=a.backgroundColor,t.fillRect(i,m+1,l-2,r-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:o}=this,{bodySpacing:n,bodyAlign:a,displayColors:s,boxHeight:r,boxWidth:l,boxPadding:c}=i,d=Gc(i.bodyFont);let h=d.lineHeight,u=0;const m=Hd(i.rtl,this.x,this.width),p=function(i){e.fillText(i,m.x(t.x+u),t.y+h/2),t.y+=h+n},_=m.textAlign(a);let g,f,b,v,y,w,x;for(e.textAlign=a,e.textBaseline="middle",e.font=d.string,t.x=vp(this,_,i),e.fillStyle=i.bodyColor,ml(this.beforeBody,p),u=s&&"right"!==_?"center"===a?l/2+c:l+2+c:0,v=0,w=o.length;v0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,o=i&&i.x,n=i&&i.y;if(o||n){const i=hp[t.position].call(this,this._active,this._eventPosition);if(!i)return;const a=this._size=_p(this,t),s=Object.assign({},i,this._size),r=fp(e,t,s),l=bp(t,s,r,e);o._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=a.width,this.height=a.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const o={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const a=Kc(e.padding),s=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&s&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,o,e),Rd(t,e.textDirection),n.y+=a.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Id(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,o=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!pl(i,o),a=this._positionChanged(o,e);(n||a)&&(this._active=o,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const o=this.options,n=this._active||[],a=this._getActiveElements(t,n,e,i),s=this._positionChanged(a,t),r=e||!pl(a,n)||s;return r&&(this._active=a,(o.enabled||o.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,o){const n=this.options;if("mouseout"===t.type)return[];if(!o)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const a=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&a.reverse(),a}_positionChanged(t,e){const{caretX:i,caretY:o,options:n}=this,a=hp[n.position].call(this,t,e);return!1!==a&&(i!==a.x||o!==a.y)}}var Sp={id:"tooltip",_element:Tp,positioners:hp,afterInit(t,e,i){i&&(t.tooltip=new Tp({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:xp},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]},Mp=Object.freeze({__proto__:null,Colors:Om,Decimation:Rm,Filler:ip,Legend:sp,SubTitle:dp,Title:lp,Tooltip:Sp});function Dp(t,e,i,o){const n=t.indexOf(e);if(-1===n)return((t,e,i,o)=>("string"==typeof e?(i=t.push(e)-1,o.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,o);return n!==t.lastIndexOf(e)?i:n}function Cp(t){const e=this.getLabels();return t>=0&&tnull===t?null:Xl(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:Dp(i,t,dl(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:o}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(o=this.getLabels().length-1)),this.min=i,this.max=o}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,o=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)o.push({value:i});return o}getLabelForValue(t){return Cp.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}}function Pp(t,e){const i=[],{bounds:o,step:n,min:a,max:s,precision:r,count:l,maxTicks:c,maxDigits:d,includeBounds:h}=t,u=n||1,m=c-1,{min:p,max:_}=e,g=!al(a),f=!al(s),b=!al(l),v=(_-p)/(d+1);let y,w,x,k,T=Rl((_-p)/m/u)*u;if(T<1e-14&&!g&&!f)return[{value:p},{value:_}];k=Math.ceil(_/T)-Math.floor(p/T),k>m&&(T=Rl(k*T/m/u)*u),al(r)||(y=Math.pow(10,r),T=Math.ceil(T*y)/y),"ticks"===o?(w=Math.floor(p/T)*T,x=Math.ceil(_/T)*T):(w=p,x=_),g&&f&&n&&function(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}((s-a)/n,T/1e3)?(k=Math.round(Math.min((s-a)/T,c)),T=(s-a)/k,w=a,x=s):b?(w=g?a:w,x=f?s:x,k=l-1,T=(x-w)/k):(k=(x-w)/T,k=Hl(k,Math.round(k),T/1e3)?Math.round(k):Math.ceil(k));const S=Math.max(Bl(T),Bl(w));y=Math.pow(10,al(r)?S:r),w=Math.round(w*y)/y,x=Math.round(x*y)/y;let M=0;for(g&&(h&&w!==a?(i.push({value:a}),ws)break;i.push({value:t})}return f&&h&&x!==s?i.length&&Hl(i[i.length-1].value,s,Ep(s,v,t))?i[i.length-1].value=s:i.push({value:s}):f&&x!==s||i.push({value:x}),i}function Ep(t,e,{horizontal:i,minRotation:o}){const n=Fl(o),a=(i?Math.sin(n):Math.cos(n))||.001,s=.75*e*(""+t).length;return Math.min(e/a,s)}class Lp extends Du{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(t,e){return al(t)||("number"==typeof t||t instanceof Number)&&!isFinite(+t)?null:+t}handleTickRangeOptions(){const{beginAtZero:t}=this.options,{minDefined:e,maxDefined:i}=this.getUserBounds();let{min:o,max:n}=this;const a=t=>o=e?o:t,s=t=>n=i?n:t;if(t){const t=Vl(o),e=Vl(n);t<0&&e<0?s(0):t>0&&e>0&&a(0)}if(o===n){let e=0===n?1:Math.abs(.05*n);s(n+e),t||a(o-e)}this.min=o,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:o}=t;return o?(e=Math.ceil(this.max/o)-Math.floor(this.min/o)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${o} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const o=Pp({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return"ticks"===t.bounds&&Nl(o,this,"value"),t.reverse?(o.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),o}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const o=(i-e)/Math.max(t.length-1,1)/2;e-=o,i+=o}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return vc(t,this.chart.options.locale,this.options.ticks.format)}}class $p extends Lp{static id="linear";static defaults={ticks:{callback:wc.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=ll(t)?t:0,this.max=ll(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=Fl(this.options.ticks.minRotation),o=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/o))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const zp=t=>Math.floor(Ol(t)),Op=(t,e)=>Math.pow(10,zp(t)+e);function Vp(t){return 1===t/Math.pow(10,zp(t))}function Hp(t,e,i){const o=Math.pow(10,i),n=Math.floor(t/o);return Math.ceil(e/o)-n}function Rp(t,{min:e,max:i}){e=cl(t.min,e);const o=[],n=zp(e);let a=function(t,e){let i=zp(e-t);for(;Hp(t,e,i)>10;)i++;for(;Hp(t,e,i)<10;)i--;return Math.min(i,zp(t))}(e,i),s=a<0?Math.pow(10,Math.abs(a)):1;const r=Math.pow(10,a),l=n>a?Math.pow(10,n):0,c=Math.round((e-l)*s)/s,d=Math.floor((e-l)/r/10)*r*10;let h=Math.floor((c-d)/Math.pow(10,a)),u=cl(t.min,Math.round((l+d+h*Math.pow(10,a))*s)/s);for(;u=10?h=h<15?15:20:h++,h>=20&&(a++,h=2,s=a>=0?1:s),u=Math.round((l+d+h*Math.pow(10,a))*s)/s;const m=cl(t.max,u);return o.push({value:m,major:Vp(m),significand:h}),o}class Ip extends Du{static id="logarithmic";static defaults={ticks:{callback:wc.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=Lp.prototype.parse.apply(this,[t,e]);if(0!==i)return ll(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=ll(t)?Math.max(0,t):null,this.max=ll(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!ll(this._userMin)&&(this.min=t===Op(this.min,0)?Op(this.min,-1):Op(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,o=this.max;const n=e=>i=t?i:e,a=t=>o=e?o:t;i===o&&(i<=0?(n(1),a(10)):(n(Op(i,-1)),a(Op(o,1)))),i<=0&&n(Op(o,-1)),o<=0&&a(Op(i,1)),this.min=i,this.max=o}buildTicks(){const t=this.options,e=Rp({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&Nl(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":vc(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=Ol(t),this._valueRange=Ol(this.max)-Ol(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(Ol(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function Np(t){const e=t.ticks;if(e.display&&t.display){const t=Kc(e.backdropPadding);return dl(e.font&&e.font.size,Dc.font.size)+t.height}return 0}function Fp(t,e,i,o,n){return t===o||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function Wp(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),o=[],n=[],a=t._pointLabels.length,s=t.options.pointLabels,r=s.centerPointLabels?Dl/a:0;for(let h=0;he.r&&(r=(o.end-e.r)/a,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/s,t.b=Math.max(t.b,e.b+l))}function jp(t,e,i){const o=t.drawingArea,{extra:n,additionalAngle:a,padding:s,size:r}=i,l=t.getPointPosition(e,o+n+s,a),c=Math.round(Wl(ql(l.angle+Ll))),d=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,c),h=function(t){if(0===t||180===t)return"center";if(t<180)return"left";return"right"}(c),u=function(t,e,i){"right"===i?t-=e:"center"===i&&(t-=e/2);return t}(l.x,r.w,h);return{visible:!0,x:l.x,y:d,textAlign:h,left:u,top:d,right:u+r.w,bottom:d+r.h}}function Up(t,e){if(!e)return!0;const{left:i,top:o,right:n,bottom:a}=t;return!(zc({x:i,y:o},e)||zc({x:i,y:a},e)||zc({x:n,y:o},e)||zc({x:n,y:a},e))}function Yp(t,e,i){const{left:o,top:n,right:a,bottom:s}=i,{backdropColor:r}=e;if(!al(r)){const i=Xc(e.borderRadius),l=Kc(e.backdropPadding);t.fillStyle=r;const c=o-l.left,d=n-l.top,h=a-o+l.width,u=s-n+l.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),Wc(t,{x:c,y:d,w:h,h:u,radius:i}),t.fill()):t.fillRect(c,d,h,u)}}function qp(t,e,i,o){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,Cl);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let a=1;at,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=Kc(Np(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=ll(t)&&!isNaN(t)?t:0,this.max=ll(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/Np(this.options))}generateTickLabels(t){Lp.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=ul(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Wp(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,o){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-o)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,o))}getIndexAngle(t){return ql(t*(Cl/(this._pointLabels.length||1))+Fl(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(al(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(al(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const a=o.setContext(t.getPointLabelContext(n));Yp(i,a,e);const s=Gc(a.font),{x:r,y:l,textAlign:c}=e;Fc(i,t._pointLabels[n],r,l+s.lineHeight/2,s,{color:a.color,textAlign:c,textBaseline:"middle"})}}(this,a),o.display&&this.ticks.forEach(((t,e)=>{if(0!==e||0===e&&this.min<0){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),s=o.setContext(i),l=n.setContext(i);!function(t,e,i,o,n){const a=t.ctx,s=e.circular,{color:r,lineWidth:l}=e;!s&&!o||!r||!l||i<0||(a.save(),a.strokeStyle=r,a.lineWidth=l,a.setLineDash(n.dash||[]),a.lineDashOffset=n.dashOffset,a.beginPath(),qp(t,i,s,o),a.closePath(),a.stroke(),a.restore())}(this,s,r,a,l)}})),i.display){for(t.save(),s=a-1;s>=0;s--){const o=i.setContext(this.getPointLabelContext(s)),{color:n,lineWidth:a}=o;a&&n&&(t.lineWidth=a,t.strokeStyle=n,t.setLineDash(o.borderDash),t.lineDashOffset=o.borderDashOffset,r=this.getDistanceFromCenterForValue(e.reverse?this.min:this.max),l=this.getPointPosition(s,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const o=this.getIndexAngle(0);let n,a;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(o),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((o,s)=>{if(0===s&&this.min>=0&&!e.reverse)return;const r=i.setContext(this.getContext(s)),l=Gc(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[s].value),r.showLabelBackdrop){t.font=l.string,a=t.measureText(o.label).width,t.fillStyle=r.backdropColor;const e=Kc(r.backdropPadding);t.fillRect(-a/2-e.left,-n-l.size/2-e.top,a+e.width,l.size+e.height)}Fc(t,o.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Xp={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Kp=Object.keys(Xp);function Gp(t,e){return t-e}function Qp(t,e){if(al(e))return null;const i=t._adapter,{parser:o,round:n,isoWeekday:a}=t._parseOpts;let s=e;return"function"==typeof o&&(s=o(s)),ll(s)||(s="string"==typeof o?i.parse(s,o):i.parse(s)),null===s?null:(n&&(s="week"!==n||!Il(a)&&!0!==a?i.startOf(s,n):i.startOf(s,"isoWeek",a)),+s)}function Jp(t,e,i,o){const n=Kp.length;for(let a=Kp.indexOf(t);a=e?i[o]:i[n]]=!0}}else t[e]=!0}function e_(t,e,i){const o=[],n={},a=e.length;let s,r;for(s=0;s=0&&(e[l].major=!0);return e}(t,o,n,i):o}class i_ extends Du{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),o=this._adapter=new Lh._date(t.adapters.date);o.init(e),vl(i.displayFormats,o.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Qp(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:o,max:n,minDefined:a,maxDefined:s}=this.getUserBounds();function r(t){a||isNaN(t.min)||(o=Math.min(o,t.min)),s||isNaN(t.max)||(n=Math.max(n,t.max))}a&&s||(r(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||r(this.getMinMax(!1))),o=ll(o)&&!isNaN(o)?o:+e.startOf(Date.now(),i),n=ll(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(o,n-1),this.max=Math.max(o+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,o="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&o.length&&(this.min=this._userMin||o[0],this.max=this._userMax||o[o.length-1]);const n=this.min,a=function(t,e,i){let o=0,n=t.length;for(;oo&&t[n-1]>i;)n--;return o>0||n=Kp.indexOf(i);a--){const i=Kp[a];if(Xp[i].common&&t._adapter.diff(n,o,i)>=e-1)return i}return Kp[i?Kp.indexOf(i):0]}(this,a.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=Kp.indexOf(t)+1,i=Kp.length;e+t.value)))}initOffsets(t=[]){let e,i,o=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),o=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const a=t.length<3?.5:.25;o=Xl(o,0,a),n=Xl(n,0,a),this._offsets={start:o,end:n,factor:1/(o+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,o=this.options,n=o.time,a=n.unit||Jp(n.minUnit,e,i,this._getLabelCapacity(e)),s=dl(o.ticks.stepSize,1),r="week"===a&&n.isoWeekday,l=Il(r)||!0===r,c={};let d,h,u=e;if(l&&(u=+t.startOf(u,"isoWeek",r)),u=+t.startOf(u,l?"day":a),t.diff(i,e,a)>1e5*s)throw new Error(e+" and "+i+" are too far apart with stepSize of "+s+" "+a);const m="data"===o.ticks.source&&this.getDataTimestamps();for(d=u,h=0;d+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,o=this._unit,n=e||i[o];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,o){const n=this.options,a=n.ticks.callback;if(a)return ul(a,[t,e,i],this);const s=n.time.displayFormats,r=this._unit,l=this._majorUnit,c=r&&s[r],d=l&&s[l],h=i[e],u=l&&d&&h&&h.major;return this._adapter.format(t,o||(u?d:c))}generateTickLabels(t){let e,i,o;for(e=0,i=t.length;e0?s:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const o=this.getMatchingVisibleMetas();if(this._normalized&&o.length)return this._cache.data=o[0].controller.getAllParsedValues(this);for(t=0,e=o.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=Ql(t,"pos",e)),({pos:o,time:a}=t[r]),({pos:n,time:s}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=Ql(t,"time",e)),({time:o,pos:a}=t[r]),({time:n,pos:s}=t[l]));const c=n-o;return c?a+(s-a)*(e-o)/c:a}class n_ extends i_{static id="timeseries";static defaults=i_.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=o_(e,this.min),this._tableRange=o_(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,o=[],n=[];let a,s,r,l,c;for(a=0,s=t.length;a=e&&l<=i&&o.push(l);if(o.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(a=0,s=o.length;at-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(o_(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return o_(this._table,i*this._tableRange+this._minPos,!0)}}var a_=Object.freeze({__proto__:null,CategoryScale:Ap,LinearScale:$p,LogarithmicScale:Ip,RadialLinearScale:Zp,TimeScale:i_,TimeSeriesScale:n_});const s_=[Ah,Cm,Mp,a_];var r_=Object.freeze({__proto__:null,Animation:Jd,Animations:th,ArcElement:cm,BarController:wh,BarElement:Dm,BasePlatform:Jh,BasicPlatform:tu,BubbleController:xh,CategoryScale:Ap,Chart:om,Colors:Om,DatasetController:uh,Decimation:Rm,DomPlatform:_u,DoughnutController:kh,Element:fu,Filler:ip,Interaction:Ih,Legend:sp,LineController:Th,LineElement:bm,LinearScale:$p,LogarithmicScale:Ip,PieController:Mh,PointElement:ym,PolarAreaController:Sh,RadarController:Dh,RadialLinearScale:Zp,Scale:Du,ScatterController:Ch,SubTitle:dp,Ticks:wc,TimeScale:i_,TimeSeriesScale:n_,Title:lp,Tooltip:Sp,_adapters:Lh,_detectPlatform:gu,animator:Kd,controllers:Ah,defaults:Dc,elements:Cm,layouts:Qh,plugins:Mp,registerables:s_,registry:Pu,scales:a_});function l_(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var c_,d_={exports:{}};c_=d_,function(t,e,i,o){var n,a=["","webkit","Moz","MS","ms","o"],s=e.createElement("div"),r="function",l=Math.round,c=Math.abs,d=Date.now;function h(t,e,i){return setTimeout(b(t,i),e)}function u(t,e,i){return!!Array.isArray(t)&&(m(t,i[e],i),!0)}function m(t,e,i){var n;if(t)if(t.forEach)t.forEach(e,i);else if(t.length!==o)for(n=0;n\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",a=t.console&&(t.console.warn||t.console.log);return a&&a.call(t.console,n,o),e.apply(this,arguments)}}n="function"!=typeof Object.assign?function(t){if(t===o||null===t)throw new TypeError("Cannot convert undefined or null to object");for(var e=Object(t),i=1;i-1}function S(t){return t.trim().split(/\s+/g)}function M(t,e,i){if(t.indexOf&&!i)return t.indexOf(e);for(var o=0;oi[e]})):o.sort()),o}function A(t,e){for(var i,n,s=e[0].toUpperCase()+e.slice(1),r=0;r1&&!i.firstMultiple?i.firstMultiple=J(e):1===a&&(i.firstMultiple=!1);var s=i.firstInput,r=i.firstMultiple,l=r?r.center:s.center,h=e.center=tt(n);e.timeStamp=d(),e.deltaTime=e.timeStamp-s.timeStamp,e.angle=nt(l,h),e.distance=ot(l,h),function(t,e){var i=e.center,o=t.offsetDelta||{},n=t.prevDelta||{},a=t.prevInput||{};e.eventType!==R&&a.eventType!==I||(n=t.prevDelta={x:a.deltaX||0,y:a.deltaY||0},o=t.offsetDelta={x:i.x,y:i.y}),e.deltaX=n.x+(i.x-o.x),e.deltaY=n.y+(i.y-o.y)}(i,e),e.offsetDirection=it(e.deltaX,e.deltaY);var u,m,p=et(e.deltaTime,e.deltaX,e.deltaY);e.overallVelocityX=p.x,e.overallVelocityY=p.y,e.overallVelocity=c(p.x)>c(p.y)?p.x:p.y,e.scale=r?(u=r.pointers,ot((m=n)[0],m[1],K)/ot(u[0],u[1],K)):1,e.rotation=r?function(t,e){return nt(e[1],e[0],K)+nt(t[1],t[0],K)}(r.pointers,n):0,e.maxPointers=i.prevInput?e.pointers.length>i.prevInput.maxPointers?e.pointers.length:i.prevInput.maxPointers:e.pointers.length,function(t,e){var i,n,a,s,r=t.lastInterval||e,l=e.timeStamp-r.timeStamp;if(e.eventType!=N&&(l>H||r.velocity===o)){var d=e.deltaX-r.deltaX,h=e.deltaY-r.deltaY,u=et(l,d,h);n=u.x,a=u.y,i=c(u.x)>c(u.y)?u.x:u.y,s=it(d,h),t.lastInterval=e}else i=r.velocity,n=r.velocityX,a=r.velocityY,s=r.direction;e.velocity=i,e.velocityX=n,e.velocityY=a,e.direction=s}(i,e);var _=t.element;k(e.srcEvent.target,_)&&(_=e.srcEvent.target),e.target=_}(t,i),t.emit("hammer.input",i),t.recognize(i),t.session.prevInput=i}function J(t){for(var e=[],i=0;i=c(e)?t<0?W:B:e<0?j:U}function ot(t,e,i){i||(i=X);var o=e[i[0]]-t[i[0]],n=e[i[1]]-t[i[1]];return Math.sqrt(o*o+n*n)}function nt(t,e,i){i||(i=X);var o=e[i[0]]-t[i[0]],n=e[i[1]]-t[i[1]];return 180*Math.atan2(n,o)/Math.PI}G.prototype={handler:function(){},init:function(){this.evEl&&w(this.element,this.evEl,this.domHandler),this.evTarget&&w(this.target,this.evTarget,this.domHandler),this.evWin&&w(E(this.element),this.evWin,this.domHandler)},destroy:function(){this.evEl&&x(this.element,this.evEl,this.domHandler),this.evTarget&&x(this.target,this.evTarget,this.domHandler),this.evWin&&x(E(this.element),this.evWin,this.domHandler)}};var at={mousedown:R,mousemove:2,mouseup:I},st="mousedown",rt="mousemove mouseup";function lt(){this.evEl=st,this.evWin=rt,this.pressed=!1,G.apply(this,arguments)}f(lt,G,{handler:function(t){var e=at[t.type];e&R&&0===t.button&&(this.pressed=!0),2&e&&1!==t.which&&(e=I),this.pressed&&(e&I&&(this.pressed=!1),this.callback(this.manager,e,{pointers:[t],changedPointers:[t],pointerType:V,srcEvent:t}))}});var ct={pointerdown:R,pointermove:2,pointerup:I,pointercancel:N,pointerout:N},dt={2:O,3:"pen",4:V,5:"kinect"},ht="pointerdown",ut="pointermove pointerup pointercancel";function mt(){this.evEl=ht,this.evWin=ut,G.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}t.MSPointerEvent&&!t.PointerEvent&&(ht="MSPointerDown",ut="MSPointerMove MSPointerUp MSPointerCancel"),f(mt,G,{handler:function(t){var e=this.store,i=!1,o=t.type.toLowerCase().replace("ms",""),n=ct[o],a=dt[t.pointerType]||t.pointerType,s=a==O,r=M(e,t.pointerId,"pointerId");n&R&&(0===t.button||s)?r<0&&(e.push(t),r=e.length-1):n&(I|N)&&(i=!0),r<0||(e[r]=t,this.callback(this.manager,n,{pointers:e,changedPointers:[t],pointerType:a,srcEvent:t}),i&&e.splice(r,1))}});var pt={touchstart:R,touchmove:2,touchend:I,touchcancel:N};function _t(){this.evTarget="touchstart",this.evWin="touchstart touchmove touchend touchcancel",this.started=!1,G.apply(this,arguments)}function gt(t,e){var i=D(t.touches),o=D(t.changedTouches);return e&(I|N)&&(i=C(i.concat(o),"identifier",!0)),[i,o]}f(_t,G,{handler:function(t){var e=pt[t.type];if(e===R&&(this.started=!0),this.started){var i=gt.call(this,t,e);e&(I|N)&&i[0].length-i[1].length==0&&(this.started=!1),this.callback(this.manager,e,{pointers:i[0],changedPointers:i[1],pointerType:O,srcEvent:t})}}});var ft={touchstart:R,touchmove:2,touchend:I,touchcancel:N},bt="touchstart touchmove touchend touchcancel";function vt(){this.evTarget=bt,this.targetIds={},G.apply(this,arguments)}function yt(t,e){var i=D(t.touches),o=this.targetIds;if(e&(2|R)&&1===i.length)return o[i[0].identifier]=!0,[i,i];var n,a,s=D(t.changedTouches),r=[],l=this.target;if(a=i.filter((function(t){return k(t.target,l)})),e===R)for(n=0;n-1&&o.splice(t,1)}),wt)}}function St(t){for(var e=t.srcEvent.clientX,i=t.srcEvent.clientY,o=0;o-1&&this.requireFail.splice(e,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(t){return!!this.simultaneous[t.id]},emit:function(t){var e=this,i=this.state;function o(i){e.manager.emit(i,t)}i<8&&o(e.options.event+It(i)),o(e.options.event),t.additionalEvent&&o(t.additionalEvent),i>=8&&o(e.options.event+It(i))},tryEmit:function(t){if(this.canEmit())return this.emit(t);this.state=Ht},canEmit:function(){for(var t=0;te.threshold&&n&e.direction},attrTest:function(t){return Wt.prototype.attrTest.call(this,t)&&(2&this.state||!(2&this.state)&&this.directionTest(t))},emit:function(t){this.pX=t.deltaX,this.pY=t.deltaY;var e=Nt(t.direction);e&&(t.additionalEvent=this.options.event+e),this._super.emit.call(this,t)}}),f(jt,Wt,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[Et]},attrTest:function(t){return this._super.attrTest.call(this,t)&&(Math.abs(t.scale-1)>this.options.threshold||2&this.state)},emit:function(t){if(1!==t.scale){var e=t.scale<1?"in":"out";t.additionalEvent=this.options.event+e}this._super.emit.call(this,t)}}),f(Ut,Rt,{defaults:{event:"press",pointers:1,time:251,threshold:9},getTouchAction:function(){return[At]},process:function(t){var e=this.options,i=t.pointers.length===e.pointers,o=t.distancee.time;if(this._input=t,!o||!i||t.eventType&(I|N)&&!n)this.reset();else if(t.eventType&R)this.reset(),this._timer=h((function(){this.state=8,this.tryEmit()}),e.time,this);else if(t.eventType&I)return 8;return Ht},reset:function(){clearTimeout(this._timer)},emit:function(t){8===this.state&&(t&&t.eventType&I?this.manager.emit(this.options.event+"up",t):(this._input.timeStamp=d(),this.manager.emit(this.options.event,this._input)))}}),f(Yt,Wt,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[Et]},attrTest:function(t){return this._super.attrTest.call(this,t)&&(Math.abs(t.rotation)>this.options.threshold||2&this.state)}}),f(qt,Wt,{defaults:{event:"swipe",threshold:10,velocity:.3,direction:Y|q,pointers:1},getTouchAction:function(){return Bt.prototype.getTouchAction.call(this)},attrTest:function(t){var e,i=this.options.direction;return i&(Y|q)?e=t.overallVelocity:i&Y?e=t.overallVelocityX:i&q&&(e=t.overallVelocityY),this._super.attrTest.call(this,t)&&i&t.offsetDirection&&t.distance>this.options.threshold&&t.maxPointers==this.options.pointers&&c(e)>this.options.velocity&&t.eventType&I},emit:function(t){var e=Nt(t.offsetDirection);e&&this.manager.emit(this.options.event+e,t),this.manager.emit(this.options.event,t)}}),f(Zt,Rt,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[Pt]},process:function(t){var e=this.options,i=t.pointers.length===e.pointers,o=t.distancet&&t.enabled&&t.modifierKey,m_=(t,e)=>t&&e[t+"Key"],p_=(t,e)=>t&&!e[t+"Key"];function __(t,e,i){return void 0===t||("string"==typeof t?-1!==t.indexOf(e):"function"==typeof t&&-1!==t({chart:i}).indexOf(e))}function g_(t,e){return"function"==typeof t&&(t=t({chart:e})),"string"==typeof t?{x:-1!==t.indexOf("x"),y:-1!==t.indexOf("y")}:{x:!1,y:!1}}function f_(t,e,i){const{mode:o="xy",scaleMode:n,overScaleMode:a}=t||{},s=function({x:t,y:e},i){const o=i.scales,n=Object.keys(o);for(let i=0;i=a.top&&e<=a.bottom&&t>=a.left&&t<=a.right)return a}return null}(e,i),r=g_(o,i),l=g_(n,i);if(a){const t=g_(a,i);for(const e of["x","y"])t[e]&&(l[e]=r[e],r[e]=!1)}if(s&&l[s.axis])return[s];const c=[];return ml(i.scales,(function(t){r[t.axis]&&c.push(t)})),c}const b_=new WeakMap;function v_(t){let e=b_.get(t);return e||(e={originalScaleLimits:{},updatedScaleLimits:{},handlers:{},panDelta:{},dragging:!1,panning:!1},b_.set(t,e)),e}function y_(t,e,i,o){const n=Math.max(0,Math.min(1,(t-e)/i||0));return{min:o*n,max:o*(1-n)}}function w_(t,e){const i=t.isHorizontal()?e.x:e.y;return t.getValueForPixel(i)}function x_(t,e,i){const o=t.max-t.min,n=o*(e-1);return y_(w_(t,i),t.min,o,n)}function k_(t,e,i,o,n){let a=i[o];if("original"===a){const i=t.originalScaleLimits[e.id][o];a=dl(i.options,i.scale)}return dl(a,n)}function T_(t,{min:e,max:i},o,n=!1){const a=v_(t.chart),{options:s}=t,r=function(t,e){return e&&(e[t.id]||e[t.axis])||{}}(t,o),{minRange:l=0}=r,c=k_(a,t,r,"min",-1/0),d=k_(a,t,r,"max",1/0);if("pan"===n&&(ed))return!0;const h=t.max-t.min,u=n?Math.max(i-e,l):h;if(n&&u===l&&h<=l)return!0;const m=function(t,{min:e,max:i,minLimit:o,maxLimit:n},a){const s=(t-i+e)/2;e-=s,i+=s;const r=a.min.options??a.min.scale,l=a.max.options??a.max.scale,c=t/1e6;return Hl(e,r,c)&&(e=r),Hl(i,l,c)&&(i=l),en&&(i=n,e=Math.max(n-t,o)),{min:e,max:i}}(u,{min:e,max:i,minLimit:c,maxLimit:d},a.originalScaleLimits[t.id]);return s.min=m.min,s.max=m.max,a.updatedScaleLimits[t.id]=m,t.parse(m.min)!==t.min||t.parse(m.max)!==t.max}const S_=t=>0===t||isNaN(t)?0:t<0?Math.min(Math.round(t),-1):Math.max(Math.round(t),1);const M_={second:500,minute:3e4,hour:18e5,day:432e5,week:3024e5,month:1296e6,quarter:5184e6,year:157248e5};function D_(t,e,i,o=!1){const{min:n,max:a,options:s}=t,r=s.time&&s.time.round,l=M_[r]||0,c=t.getValueForPixel(t.getPixelForValue(n+l)-e),d=t.getValueForPixel(t.getPixelForValue(a+l)-e);return!(!isNaN(c)&&!isNaN(d))||T_(t,{min:c,max:d},i,!!o&&"pan")}function C_(t,e,i){return D_(t,e,i,!0)}const A_={category:function(t,e,i,o){const n=x_(t,e,i);return t.min===t.max&&e<1&&function(t){const e=t.getLabels().length-1;t.min>0&&(t.min-=1),t.maxr&&(n=Math.max(0,n-l),a=1===s?n:n+s,c=0===n),T_(t,{min:n,max:a},i)||c},default:D_,logarithmic:C_,timeseries:C_};function L_(t,e){ml(t,((i,o)=>{e[o]||delete t[o]}))}function $_(t,e){const{scales:i}=t,{originalScaleLimits:o,updatedScaleLimits:n}=e;return ml(i,(function(t){(function(t,e,i){const{id:o,options:{min:n,max:a}}=t;if(!e[o]||!i[o])return!0;const s=i[o];return s.min!==n||s.max!==a})(t,o,n)&&(o[t.id]={min:{scale:t.min,options:t.options.min},max:{scale:t.max,options:t.options.max}})})),L_(o,i),L_(n,i),o}function z_(t,e,i,o){ul(A_[t.type]||A_.default,[t,e,i,o])}function O_(t,e,i,o){ul(P_[t.type]||P_.default,[t,e,i,o])}function V_(t){const e=t.chartArea;return{x:(e.left+e.right)/2,y:(e.top+e.bottom)/2}}function H_(t,e,i="none",o="api"){const{x:n=1,y:a=1,focalPoint:s=V_(t)}="number"==typeof e?{x:e,y:e}:e,r=v_(t),{options:{limits:l,zoom:c}}=r;$_(t,r);const d=1!==n,h=1!==a;ml(f_(c,s,t)||t.scales,(function(t){t.isHorizontal()&&d?z_(t,n,s,l):!t.isHorizontal()&&h&&z_(t,a,s,l)})),t.update(i),ul(c.onZoom,[{chart:t,trigger:o}])}function R_(t,e,i,o="none",n="api"){const a=v_(t),{options:{limits:s,zoom:r}}=a,{mode:l="xy"}=r;$_(t,a);const c=__(l,"x",t),d=__(l,"y",t);ml(t.scales,(function(t){t.isHorizontal()&&c?O_(t,e.x,i.x,s):!t.isHorizontal()&&d&&O_(t,e.y,i.y,s)})),t.update(o),ul(r.onZoom,[{chart:t,trigger:n}])}function I_(t,e,i,o="none",n="api"){const a=v_(t);$_(t,a);T_(t.scales[e],i,void 0,!0),t.update(o),ul(a.options.zoom?.onZoom,[{chart:t,trigger:n}])}function N_(t,e="default"){const i=v_(t),o=$_(t,i);ml(t.scales,(function(t){const e=t.options;o[t.id]?(e.min=o[t.id].min.options,e.max=o[t.id].max.options):(delete e.min,delete e.max),delete i.updatedScaleLimits[t.id]})),t.update(e),ul(i.options.zoom.onZoomComplete,[{chart:t}])}function F_(t){const e=v_(t);let i=1,o=1;return ml(t.scales,(function(t){const n=function(t,e){const i=t.originalScaleLimits[e];if(!i)return;const{min:o,max:n}=i;return dl(n.options,n.scale)-dl(o.options,o.scale)}(e,t.id);if(n){const e=Math.round(n/(t.max-t.min)*100)/100;i=Math.min(i,e),o=Math.max(o,e)}})),i<1?i:o}function W_(t,e,i,o){const{panDelta:n}=o,a=n[t.id]||0;Vl(a)===Vl(e)&&(e+=a);ul(E_[t.type]||E_.default,[t,e,i])?n[t.id]=0:n[t.id]=e}function B_(t,e,i,o="none"){const{x:n=0,y:a=0}="number"==typeof e?{x:e,y:e}:e,s=v_(t),{options:{pan:r,limits:l}}=s,{onPan:c}=r||{};$_(t,s);const d=0!==n,h=0!==a;ml(i||t.scales,(function(t){t.isHorizontal()&&d?W_(t,n,l,s):!t.isHorizontal()&&h&&W_(t,a,l,s)})),t.update(o),ul(c,[{chart:t}])}function j_(t){const e=v_(t);$_(t,e);const i={};for(const o of Object.keys(t.scales)){const{min:t,max:n}=e.originalScaleLimits[o]||{min:{},max:{}};i[o]={min:t.scale,max:n.scale}}return i}function U_(t){const e=v_(t);return e.panning||e.dragging}const Y_=(t,e,i)=>Math.min(i,Math.max(e,t));function q_(t,e){const{handlers:i}=v_(t),o=i[e];o&&o.target&&(o.target.removeEventListener(e,o),delete i[e])}function Z_(t,e,i,o){const{handlers:n,options:a}=v_(t),s=n[i];if(s&&s.target===e)return;q_(t,i),n[i]=e=>o(t,e,a),n[i].target=e;const r="wheel"!==i&&void 0;e.addEventListener(i,n[i],{passive:r})}function X_(t,e){const i=v_(t);i.dragStart&&(i.dragging=!0,i.dragEnd=e,t.update("none"))}function K_(t,e){const i=v_(t);i.dragStart&&"Escape"===e.key&&(q_(t,"keydown"),i.dragging=!1,i.dragStart=i.dragEnd=null,t.update("none"))}function G_(t,e){if(t.target!==e.canvas){const i=e.canvas.getBoundingClientRect();return{x:t.clientX-i.left,y:t.clientY-i.top}}return Cd(t,e)}function Q_(t,e,i){const{onZoomStart:o,onZoomRejected:n}=i;if(o){if(!1===ul(o,[{chart:t,event:e,point:G_(e,t)}]))return ul(n,[{chart:t,event:e}]),!1}}function J_(t,e){if(t.legend){if(zc(Cd(e,t),t.legend))return}const i=v_(t),{pan:o,zoom:n={}}=i.options;if(0!==e.button||m_(u_(o),e)||p_(u_(n.drag),e))return ul(n.onZoomRejected,[{chart:t,event:e}]);!1!==Q_(t,e,n)&&(i.dragStart=e,Z_(t,t.canvas.ownerDocument,"mousemove",X_),Z_(t,window.document,"keydown",K_))}function tg(t,e,i,{min:o,max:n,prop:a}){t[o]=Y_(Math.min(i.begin[a],i.end[a]),e[o],e[n]),t[n]=Y_(Math.max(i.begin[a],i.end[a]),e[o],e[n])}function eg(t,e,i){const o={begin:G_(e.dragStart,t),end:G_(e.dragEnd,t)};if(i){!function({begin:t,end:e},i){let o=e.x-t.x,n=e.y-t.y;const a=Math.abs(o/n);a>i?o=Math.sign(o)*Math.abs(n*i):a=0?2-1/(1-a):1+a;H_(t,{x:s,y:s,focalPoint:{x:e.clientX-n.left,y:e.clientY-n.top}},"zoom","wheel"),ul(i,[{chart:t}])}function ag(t,e,i,o){i&&(v_(t).handlers[e]=function(t,e){let i;return function(){return clearTimeout(i),i=setTimeout(t,e),e}}((()=>ul(i,[{chart:t}])),o))}function sg(t,e){return function(i,o){const{pan:n,zoom:a={}}=e.options;if(!n||!n.enabled)return!1;const s=o&&o.srcEvent;return!s||(!(!e.panning&&"mouse"===o.pointerType&&(p_(u_(n),s)||m_(u_(a.drag),s)))||(ul(n.onPanRejected,[{chart:t,event:o}]),!1))}}function rg(t,e,i){if(e.scale){const{center:o,pointers:n}=i,a=1/e.scale*i.scale,s=i.target.getBoundingClientRect(),r=function(t,e){const i=Math.abs(t.clientX-e.clientX),o=Math.abs(t.clientY-e.clientY),n=i/o;let a,s;return n>.3&&n<1.7?a=s=!0:i>o?a=!0:s=!0,{x:a,y:s}}(n[0],n[1]),l=e.options.zoom.mode;H_(t,{x:r.x&&__(l,"x",t)?a:1,y:r.y&&__(l,"y",t)?a:1,focalPoint:{x:o.x-s.left,y:o.y-s.top}},"zoom","pinch"),e.scale=i.scale}}function lg(t,e,i){const o=e.delta;o&&(e.panning=!0,B_(t,{x:i.deltaX-o.x,y:i.deltaY-o.y},e.panScales),e.delta={x:i.deltaX,y:i.deltaY})}const cg=new WeakMap;function dg(t,e){const i=v_(t),o=t.canvas,{pan:n,zoom:a}=e,s=new h_.Manager(o);a&&a.pinch.enabled&&(s.add(new h_.Pinch),s.on("pinchstart",(e=>function(t,e,i){if(e.options.zoom.pinch.enabled){const o=Cd(i,t);!1===ul(e.options.zoom.onZoomStart,[{chart:t,event:i,point:o}])?(e.scale=null,ul(e.options.zoom.onZoomRejected,[{chart:t,event:i}])):e.scale=1}}(t,i,e))),s.on("pinch",(e=>rg(t,i,e))),s.on("pinchend",(e=>function(t,e,i){e.scale&&(rg(t,e,i),e.scale=null,ul(e.options.zoom.onZoomComplete,[{chart:t}]))}(t,i,e)))),n&&n.enabled&&(s.add(new h_.Pan({threshold:n.threshold,enable:sg(t,i)})),s.on("panstart",(e=>function(t,e,i){const{enabled:o,onPanStart:n,onPanRejected:a}=e.options.pan;if(!o)return;const s=i.target.getBoundingClientRect(),r={x:i.center.x-s.left,y:i.center.y-s.top};if(!1===ul(n,[{chart:t,event:i,point:r}]))return ul(a,[{chart:t,event:i}]);e.panScales=f_(e.options.pan,r,t),e.delta={x:0,y:0},lg(t,e,i)}(t,i,e))),s.on("panmove",(e=>lg(t,i,e))),s.on("panend",(()=>function(t,e){e.delta=null,e.panning&&(e.panning=!1,e.filterNextClick=!0,ul(e.options.pan.onPanComplete,[{chart:t}]))}(t,i)))),cg.set(t,s)}function hg(t){const e=cg.get(t);e&&(e.remove("pinchstart"),e.remove("pinch"),e.remove("pinchend"),e.remove("panstart"),e.remove("pan"),e.remove("panend"),e.destroy(),cg.delete(t))}function ug(t,e,i){const o=i.zoom.drag,{dragStart:n,dragEnd:a}=v_(t);if(o.drawTime!==e||!a)return;const{left:s,top:r,width:l,height:c}=ig(t,i.zoom.mode,{dragStart:n,dragEnd:a},o.maintainAspectRatio),d=t.ctx;d.save(),d.beginPath(),d.fillStyle=o.backgroundColor||"rgba(225,225,225,0.3)",d.fillRect(s,r,l,c),o.borderWidth>0&&(d.lineWidth=o.borderWidth,d.strokeStyle=o.borderColor||"rgba(225,225,225)",d.strokeRect(s,r,l,c)),d.restore()}var mg={id:"zoom",version:"2.2.0",defaults:{pan:{enabled:!1,mode:"xy",threshold:10,modifierKey:null},zoom:{wheel:{enabled:!1,speed:.1,modifierKey:null},drag:{enabled:!1,drawTime:"beforeDatasetsDraw",modifierKey:null},pinch:{enabled:!1},mode:"xy"}},start:function(t,e,i){v_(t).options=i,Object.prototype.hasOwnProperty.call(i.zoom,"enabled")&&console.warn("The option `zoom.enabled` is no longer supported. Please use `zoom.wheel.enabled`, `zoom.drag.enabled`, or `zoom.pinch.enabled`."),(Object.prototype.hasOwnProperty.call(i.zoom,"overScaleMode")||Object.prototype.hasOwnProperty.call(i.pan,"overScaleMode"))&&console.warn("The option `overScaleMode` is deprecated. Please use `scaleMode` instead (and update `mode` as desired)."),h_&&dg(t,i),t.pan=(e,i,o)=>B_(t,e,i,o),t.zoom=(e,i)=>H_(t,e,i),t.zoomRect=(e,i,o)=>R_(t,e,i,o),t.zoomScale=(e,i,o)=>I_(t,e,i,o),t.resetZoom=e=>N_(t,e),t.getZoomLevel=()=>F_(t),t.getInitialScaleBounds=()=>j_(t),t.getZoomedScaleBounds=()=>function(t){const e=v_(t),i={};for(const o of Object.keys(t.scales))i[o]=e.updatedScaleLimits[o];return i}(t),t.isZoomedOrPanned=()=>function(t){const e=j_(t);for(const i of Object.keys(t.scales)){const{min:o,max:n}=e[i];if(void 0!==o&&t.scales[i].min!==o)return!0;if(void 0!==n&&t.scales[i].max!==n)return!0}return!1}(t),t.isZoomingOrPanning=()=>U_(t)},beforeEvent(t,{event:e}){if(U_(t))return!1;if("click"===e.type||"mouseup"===e.type){const e=v_(t);if(e.filterNextClick)return e.filterNextClick=!1,!1}},beforeUpdate:function(t,e,i){const o=v_(t),n=o.options;o.options=i,function(t,e){const{pan:i,zoom:o}=t,{pan:n,zoom:a}=e;return o?.zoom?.pinch?.enabled!==a?.zoom?.pinch?.enabled||i?.enabled!==n?.enabled||i?.threshold!==n?.threshold}(n,i)&&(hg(t),dg(t,i)),function(t,e){const i=t.canvas,{wheel:o,drag:n,onZoomComplete:a}=e.zoom;o.enabled?(Z_(t,i,"wheel",ng),ag(t,"onZoomComplete",a,250)):q_(t,"wheel"),n.enabled?(Z_(t,i,"mousedown",J_),Z_(t,i.ownerDocument,"mouseup",og)):(q_(t,"mousedown"),q_(t,"mousemove"),q_(t,"mouseup"),q_(t,"keydown"))}(t,i)},beforeDatasetsDraw(t,e,i){ug(t,"beforeDatasetsDraw",i)},afterDatasetsDraw(t,e,i){ug(t,"afterDatasetsDraw",i)},beforeDraw(t,e,i){ug(t,"beforeDraw",i)},afterDraw(t,e,i){ug(t,"afterDraw",i)},stop:function(t){!function(t){q_(t,"mousedown"),q_(t,"mousemove"),q_(t,"mouseup"),q_(t,"wheel"),q_(t,"click"),q_(t,"keydown")}(t),h_&&hg(t),function(t){b_.delete(t)}(t)},panFunctions:E_,zoomFunctions:A_,zoomRectFunctions:P_},pg=Object.freeze({__proto__:null,default:mg,pan:B_,resetZoom:N_,zoom:H_,zoomRect:R_,zoomScale:I_});function _g(t){const e=Object.prototype.toString.call(t);return t instanceof Date||"object"==typeof t&&"[object Date]"===e?new t.constructor(+t):"number"==typeof t||"[object Number]"===e||"string"==typeof t||"[object String]"===e?new Date(t):new Date(NaN)}function gg(t,e){return t instanceof Date?new t.constructor(e):new Date(e)}function fg(t,e){const i=_g(t);return isNaN(e)?gg(t,NaN):e?(i.setDate(i.getDate()+e),i):i}function bg(t,e){const i=_g(t);if(isNaN(e))return gg(t,NaN);if(!e)return i;const o=i.getDate(),n=gg(t,i.getTime());n.setMonth(i.getMonth()+e+1,0);return o>=n.getDate()?n:(i.setFullYear(n.getFullYear(),n.getMonth(),o),i)}function vg(t,e){return gg(t,+_g(t)+e)}const yg=6048e5,wg=864e5,xg=6e4,kg=36e5;let Tg={};function Sg(){return Tg}function Mg(t,e){const i=Sg(),o=e?.weekStartsOn??e?.locale?.options?.weekStartsOn??i.weekStartsOn??i.locale?.options?.weekStartsOn??0,n=_g(t),a=n.getDay(),s=(a=n.getTime()?i+1:e.getTime()>=s.getTime()?i:i-1}function Ag(t){const e=_g(t);return e.setHours(0,0,0,0),e}function Pg(t){const e=_g(t),i=new Date(Date.UTC(e.getFullYear(),e.getMonth(),e.getDate(),e.getHours(),e.getMinutes(),e.getSeconds(),e.getMilliseconds()));return i.setUTCFullYear(e.getFullYear()),+t-+i}function Eg(t,e){const i=Ag(t),o=Ag(e),n=+i-Pg(i),a=+o-Pg(o);return Math.round((n-a)/wg)}function Lg(t,e){const i=_g(t),o=_g(e),n=i.getTime()-o.getTime();return n<0?-1:n>0?1:n}function $g(t){if(!(e=t,e instanceof Date||"object"==typeof e&&"[object Date]"===Object.prototype.toString.call(e)||"number"==typeof t))return!1;var e;const i=_g(t);return!isNaN(Number(i))}function zg(t,e){const i=_g(t),o=_g(e),n=Og(i,o),a=Math.abs(Eg(i,o));i.setDate(i.getDate()-n*a);const s=n*(a-Number(Og(i,o)===-n));return 0===s?0:s}function Og(t,e){const i=t.getFullYear()-e.getFullYear()||t.getMonth()-e.getMonth()||t.getDate()-e.getDate()||t.getHours()-e.getHours()||t.getMinutes()-e.getMinutes()||t.getSeconds()-e.getSeconds()||t.getMilliseconds()-e.getMilliseconds();return i<0?-1:i>0?1:i}function Vg(t){return e=>{const i=(t?Math[t]:Math.trunc)(e);return 0===i?0:i}}function Hg(t,e){return+_g(t)-+_g(e)}function Rg(t){const e=_g(t);return e.setHours(23,59,59,999),e}function Ig(t){const e=_g(t),i=e.getMonth();return e.setFullYear(e.getFullYear(),i+1,0),e.setHours(23,59,59,999),e}function Ng(t,e){const i=_g(t),o=_g(e),n=Lg(i,o),a=Math.abs(function(t,e){const i=_g(t),o=_g(e);return 12*(i.getFullYear()-o.getFullYear())+(i.getMonth()-o.getMonth())}(i,o));let s;if(a<1)s=0;else{1===i.getMonth()&&i.getDate()>27&&i.setDate(30),i.setMonth(i.getMonth()-n*a);let e=Lg(i,o)===-n;(function(t){const e=_g(t);return+Rg(e)==+Ig(e)})(_g(t))&&1===a&&1===Lg(t,o)&&(e=!1),s=n*(a-Number(e))}return 0===s?0:s}function Fg(t,e){const i=_g(t),o=_g(e),n=Lg(i,o),a=Math.abs(function(t,e){const i=_g(t),o=_g(e);return i.getFullYear()-o.getFullYear()}(i,o));i.setFullYear(1584),o.setFullYear(1584);const s=n*(a-+(Lg(i,o)===-n));return 0===s?0:s}function Wg(t){const e=_g(t),i=gg(t,0);return i.setFullYear(e.getFullYear(),0,1),i.setHours(0,0,0,0),i}const Bg={lessThanXSeconds:{one:"less than a second",other:"less than {{count}} seconds"},xSeconds:{one:"1 second",other:"{{count}} seconds"},halfAMinute:"half a minute",lessThanXMinutes:{one:"less than a minute",other:"less than {{count}} minutes"},xMinutes:{one:"1 minute",other:"{{count}} minutes"},aboutXHours:{one:"about 1 hour",other:"about {{count}} hours"},xHours:{one:"1 hour",other:"{{count}} hours"},xDays:{one:"1 day",other:"{{count}} days"},aboutXWeeks:{one:"about 1 week",other:"about {{count}} weeks"},xWeeks:{one:"1 week",other:"{{count}} weeks"},aboutXMonths:{one:"about 1 month",other:"about {{count}} months"},xMonths:{one:"1 month",other:"{{count}} months"},aboutXYears:{one:"about 1 year",other:"about {{count}} years"},xYears:{one:"1 year",other:"{{count}} years"},overXYears:{one:"over 1 year",other:"over {{count}} years"},almostXYears:{one:"almost 1 year",other:"almost {{count}} years"}};function jg(t){return(e={})=>{const i=e.width?String(e.width):t.defaultWidth;return t.formats[i]||t.formats[t.defaultWidth]}}const Ug={date:jg({formats:{full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},defaultWidth:"full"}),time:jg({formats:{full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},defaultWidth:"full"}),dateTime:jg({formats:{full:"{{date}} 'at' {{time}}",long:"{{date}} 'at' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},defaultWidth:"full"})},Yg={lastWeek:"'last' eeee 'at' p",yesterday:"'yesterday at' p",today:"'today at' p",tomorrow:"'tomorrow at' p",nextWeek:"eeee 'at' p",other:"P"};function qg(t){return(e,i)=>{let o;if("formatting"===(i?.context?String(i.context):"standalone")&&t.formattingValues){const e=t.defaultFormattingWidth||t.defaultWidth,n=i?.width?String(i.width):e;o=t.formattingValues[n]||t.formattingValues[e]}else{const e=t.defaultWidth,n=i?.width?String(i.width):t.defaultWidth;o=t.values[n]||t.values[e]}return o[t.argumentCallback?t.argumentCallback(e):e]}}const Zg={ordinalNumber:(t,e)=>{const i=Number(t),o=i%100;if(o>20||o<10)switch(o%10){case 1:return i+"st";case 2:return i+"nd";case 3:return i+"rd"}return i+"th"},era:qg({values:{narrow:["B","A"],abbreviated:["BC","AD"],wide:["Before Christ","Anno Domini"]},defaultWidth:"wide"}),quarter:qg({values:{narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1st quarter","2nd quarter","3rd quarter","4th quarter"]},defaultWidth:"wide",argumentCallback:t=>t-1}),month:qg({values:{narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],wide:["January","February","March","April","May","June","July","August","September","October","November","December"]},defaultWidth:"wide"}),day:qg({values:{narrow:["S","M","T","W","T","F","S"],short:["Su","Mo","Tu","We","Th","Fr","Sa"],abbreviated:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],wide:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},defaultWidth:"wide"}),dayPeriod:qg({values:{narrow:{am:"a",pm:"p",midnight:"mi",noon:"n",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},abbreviated:{am:"AM",pm:"PM",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"}},defaultWidth:"wide",formattingValues:{narrow:{am:"a",pm:"p",midnight:"mi",noon:"n",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"},abbreviated:{am:"AM",pm:"PM",midnight:"midnight",noon:"noon",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnight",noon:"noon",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"}},defaultFormattingWidth:"wide"})};function Xg(t){return(e,i={})=>{const o=i.width,n=o&&t.matchPatterns[o]||t.matchPatterns[t.defaultMatchWidth],a=e.match(n);if(!a)return null;const s=a[0],r=o&&t.parsePatterns[o]||t.parsePatterns[t.defaultParseWidth],l=Array.isArray(r)?function(t,e){for(let i=0;it.test(s))):function(t,e){for(const i in t)if(Object.prototype.hasOwnProperty.call(t,i)&&e(t[i]))return i;return}(r,(t=>t.test(s)));let c;c=t.valueCallback?t.valueCallback(l):l,c=i.valueCallback?i.valueCallback(c):c;return{value:c,rest:e.slice(s.length)}}}const Kg={ordinalNumber:(Gg={matchPattern:/^(\d+)(th|st|nd|rd)?/i,parsePattern:/\d+/i,valueCallback:t=>parseInt(t,10)},(t,e={})=>{const i=t.match(Gg.matchPattern);if(!i)return null;const o=i[0],n=t.match(Gg.parsePattern);if(!n)return null;let a=Gg.valueCallback?Gg.valueCallback(n[0]):n[0];return a=e.valueCallback?e.valueCallback(a):a,{value:a,rest:t.slice(o.length)}}),era:Xg({matchPatterns:{narrow:/^(b|a)/i,abbreviated:/^(b\.?\s?c\.?|b\.?\s?c\.?\s?e\.?|a\.?\s?d\.?|c\.?\s?e\.?)/i,wide:/^(before christ|before common era|anno domini|common era)/i},defaultMatchWidth:"wide",parsePatterns:{any:[/^b/i,/^(a|c)/i]},defaultParseWidth:"any"}),quarter:Xg({matchPatterns:{narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](th|st|nd|rd)? quarter/i},defaultMatchWidth:"wide",parsePatterns:{any:[/1/i,/2/i,/3/i,/4/i]},defaultParseWidth:"any",valueCallback:t=>t+1}),month:Xg({matchPatterns:{narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i,wide:/^(january|february|march|april|may|june|july|august|september|october|november|december)/i},defaultMatchWidth:"wide",parsePatterns:{narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^may/i,/^jun/i,/^jul/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},defaultParseWidth:"any"}),day:Xg({matchPatterns:{narrow:/^[smtwf]/i,short:/^(su|mo|tu|we|th|fr|sa)/i,abbreviated:/^(sun|mon|tue|wed|thu|fri|sat)/i,wide:/^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)/i},defaultMatchWidth:"wide",parsePatterns:{narrow:[/^s/i,/^m/i,/^t/i,/^w/i,/^t/i,/^f/i,/^s/i],any:[/^su/i,/^m/i,/^tu/i,/^w/i,/^th/i,/^f/i,/^sa/i]},defaultParseWidth:"any"}),dayPeriod:Xg({matchPatterns:{narrow:/^(a|p|mi|n|(in the|at) (morning|afternoon|evening|night))/i,any:/^([ap]\.?\s?m\.?|midnight|noon|(in the|at) (morning|afternoon|evening|night))/i},defaultMatchWidth:"any",parsePatterns:{any:{am:/^a/i,pm:/^p/i,midnight:/^mi/i,noon:/^no/i,morning:/morning/i,afternoon:/afternoon/i,evening:/evening/i,night:/night/i}},defaultParseWidth:"any"})};var Gg;const Qg={code:"en-US",formatDistance:(t,e,i)=>{let o;const n=Bg[t];return o="string"==typeof n?n:1===e?n.one:n.other.replace("{{count}}",e.toString()),i?.addSuffix?i.comparison&&i.comparison>0?"in "+o:o+" ago":o},formatLong:Ug,formatRelative:(t,e,i,o)=>Yg[t],localize:Zg,match:Kg,options:{weekStartsOn:0,firstWeekContainsDate:1}};function Jg(t){const e=_g(t),i=+Dg(e)-+function(t){const e=Cg(t),i=gg(t,0);return i.setFullYear(e,0,4),i.setHours(0,0,0,0),Dg(i)}(e);return Math.round(i/yg)+1}function tf(t,e){const i=_g(t),o=i.getFullYear(),n=Sg(),a=e?.firstWeekContainsDate??e?.locale?.options?.firstWeekContainsDate??n.firstWeekContainsDate??n.locale?.options?.firstWeekContainsDate??1,s=gg(t,0);s.setFullYear(o+1,0,a),s.setHours(0,0,0,0);const r=Mg(s,e),l=gg(t,0);l.setFullYear(o,0,a),l.setHours(0,0,0,0);const c=Mg(l,e);return i.getTime()>=r.getTime()?o+1:i.getTime()>=c.getTime()?o:o-1}function ef(t,e){const i=_g(t),o=+Mg(i,e)-+function(t,e){const i=Sg(),o=e?.firstWeekContainsDate??e?.locale?.options?.firstWeekContainsDate??i.firstWeekContainsDate??i.locale?.options?.firstWeekContainsDate??1,n=tf(t,e),a=gg(t,0);return a.setFullYear(n,0,o),a.setHours(0,0,0,0),Mg(a,e)}(i,e);return Math.round(o/yg)+1}function of(t,e){return(t<0?"-":"")+Math.abs(t).toString().padStart(e,"0")}const nf={y(t,e){const i=t.getFullYear(),o=i>0?i:1-i;return of("yy"===e?o%100:o,e.length)},M(t,e){const i=t.getMonth();return"M"===e?String(i+1):of(i+1,2)},d:(t,e)=>of(t.getDate(),e.length),a(t,e){const i=t.getHours()/12>=1?"pm":"am";switch(e){case"a":case"aa":return i.toUpperCase();case"aaa":return i;case"aaaaa":return i[0];default:return"am"===i?"a.m.":"p.m."}},h:(t,e)=>of(t.getHours()%12||12,e.length),H:(t,e)=>of(t.getHours(),e.length),m:(t,e)=>of(t.getMinutes(),e.length),s:(t,e)=>of(t.getSeconds(),e.length),S(t,e){const i=e.length,o=t.getMilliseconds();return of(Math.trunc(o*Math.pow(10,i-3)),e.length)}},af="midnight",sf="noon",rf="morning",lf="afternoon",cf="evening",df="night",hf={G:function(t,e,i){const o=t.getFullYear()>0?1:0;switch(e){case"G":case"GG":case"GGG":return i.era(o,{width:"abbreviated"});case"GGGGG":return i.era(o,{width:"narrow"});default:return i.era(o,{width:"wide"})}},y:function(t,e,i){if("yo"===e){const e=t.getFullYear(),o=e>0?e:1-e;return i.ordinalNumber(o,{unit:"year"})}return nf.y(t,e)},Y:function(t,e,i,o){const n=tf(t,o),a=n>0?n:1-n;if("YY"===e){return of(a%100,2)}return"Yo"===e?i.ordinalNumber(a,{unit:"year"}):of(a,e.length)},R:function(t,e){return of(Cg(t),e.length)},u:function(t,e){return of(t.getFullYear(),e.length)},Q:function(t,e,i){const o=Math.ceil((t.getMonth()+1)/3);switch(e){case"Q":return String(o);case"QQ":return of(o,2);case"Qo":return i.ordinalNumber(o,{unit:"quarter"});case"QQQ":return i.quarter(o,{width:"abbreviated",context:"formatting"});case"QQQQQ":return i.quarter(o,{width:"narrow",context:"formatting"});default:return i.quarter(o,{width:"wide",context:"formatting"})}},q:function(t,e,i){const o=Math.ceil((t.getMonth()+1)/3);switch(e){case"q":return String(o);case"qq":return of(o,2);case"qo":return i.ordinalNumber(o,{unit:"quarter"});case"qqq":return i.quarter(o,{width:"abbreviated",context:"standalone"});case"qqqqq":return i.quarter(o,{width:"narrow",context:"standalone"});default:return i.quarter(o,{width:"wide",context:"standalone"})}},M:function(t,e,i){const o=t.getMonth();switch(e){case"M":case"MM":return nf.M(t,e);case"Mo":return i.ordinalNumber(o+1,{unit:"month"});case"MMM":return i.month(o,{width:"abbreviated",context:"formatting"});case"MMMMM":return i.month(o,{width:"narrow",context:"formatting"});default:return i.month(o,{width:"wide",context:"formatting"})}},L:function(t,e,i){const o=t.getMonth();switch(e){case"L":return String(o+1);case"LL":return of(o+1,2);case"Lo":return i.ordinalNumber(o+1,{unit:"month"});case"LLL":return i.month(o,{width:"abbreviated",context:"standalone"});case"LLLLL":return i.month(o,{width:"narrow",context:"standalone"});default:return i.month(o,{width:"wide",context:"standalone"})}},w:function(t,e,i,o){const n=ef(t,o);return"wo"===e?i.ordinalNumber(n,{unit:"week"}):of(n,e.length)},I:function(t,e,i){const o=Jg(t);return"Io"===e?i.ordinalNumber(o,{unit:"week"}):of(o,e.length)},d:function(t,e,i){return"do"===e?i.ordinalNumber(t.getDate(),{unit:"date"}):nf.d(t,e)},D:function(t,e,i){const o=function(t){const e=_g(t);return Eg(e,Wg(e))+1}(t);return"Do"===e?i.ordinalNumber(o,{unit:"dayOfYear"}):of(o,e.length)},E:function(t,e,i){const o=t.getDay();switch(e){case"E":case"EE":case"EEE":return i.day(o,{width:"abbreviated",context:"formatting"});case"EEEEE":return i.day(o,{width:"narrow",context:"formatting"});case"EEEEEE":return i.day(o,{width:"short",context:"formatting"});default:return i.day(o,{width:"wide",context:"formatting"})}},e:function(t,e,i,o){const n=t.getDay(),a=(n-o.weekStartsOn+8)%7||7;switch(e){case"e":return String(a);case"ee":return of(a,2);case"eo":return i.ordinalNumber(a,{unit:"day"});case"eee":return i.day(n,{width:"abbreviated",context:"formatting"});case"eeeee":return i.day(n,{width:"narrow",context:"formatting"});case"eeeeee":return i.day(n,{width:"short",context:"formatting"});default:return i.day(n,{width:"wide",context:"formatting"})}},c:function(t,e,i,o){const n=t.getDay(),a=(n-o.weekStartsOn+8)%7||7;switch(e){case"c":return String(a);case"cc":return of(a,e.length);case"co":return i.ordinalNumber(a,{unit:"day"});case"ccc":return i.day(n,{width:"abbreviated",context:"standalone"});case"ccccc":return i.day(n,{width:"narrow",context:"standalone"});case"cccccc":return i.day(n,{width:"short",context:"standalone"});default:return i.day(n,{width:"wide",context:"standalone"})}},i:function(t,e,i){const o=t.getDay(),n=0===o?7:o;switch(e){case"i":return String(n);case"ii":return of(n,e.length);case"io":return i.ordinalNumber(n,{unit:"day"});case"iii":return i.day(o,{width:"abbreviated",context:"formatting"});case"iiiii":return i.day(o,{width:"narrow",context:"formatting"});case"iiiiii":return i.day(o,{width:"short",context:"formatting"});default:return i.day(o,{width:"wide",context:"formatting"})}},a:function(t,e,i){const o=t.getHours()/12>=1?"pm":"am";switch(e){case"a":case"aa":return i.dayPeriod(o,{width:"abbreviated",context:"formatting"});case"aaa":return i.dayPeriod(o,{width:"abbreviated",context:"formatting"}).toLowerCase();case"aaaaa":return i.dayPeriod(o,{width:"narrow",context:"formatting"});default:return i.dayPeriod(o,{width:"wide",context:"formatting"})}},b:function(t,e,i){const o=t.getHours();let n;switch(n=12===o?sf:0===o?af:o/12>=1?"pm":"am",e){case"b":case"bb":return i.dayPeriod(n,{width:"abbreviated",context:"formatting"});case"bbb":return i.dayPeriod(n,{width:"abbreviated",context:"formatting"}).toLowerCase();case"bbbbb":return i.dayPeriod(n,{width:"narrow",context:"formatting"});default:return i.dayPeriod(n,{width:"wide",context:"formatting"})}},B:function(t,e,i){const o=t.getHours();let n;switch(n=o>=17?cf:o>=12?lf:o>=4?rf:df,e){case"B":case"BB":case"BBB":return i.dayPeriod(n,{width:"abbreviated",context:"formatting"});case"BBBBB":return i.dayPeriod(n,{width:"narrow",context:"formatting"});default:return i.dayPeriod(n,{width:"wide",context:"formatting"})}},h:function(t,e,i){if("ho"===e){let e=t.getHours()%12;return 0===e&&(e=12),i.ordinalNumber(e,{unit:"hour"})}return nf.h(t,e)},H:function(t,e,i){return"Ho"===e?i.ordinalNumber(t.getHours(),{unit:"hour"}):nf.H(t,e)},K:function(t,e,i){const o=t.getHours()%12;return"Ko"===e?i.ordinalNumber(o,{unit:"hour"}):of(o,e.length)},k:function(t,e,i){let o=t.getHours();return 0===o&&(o=24),"ko"===e?i.ordinalNumber(o,{unit:"hour"}):of(o,e.length)},m:function(t,e,i){return"mo"===e?i.ordinalNumber(t.getMinutes(),{unit:"minute"}):nf.m(t,e)},s:function(t,e,i){return"so"===e?i.ordinalNumber(t.getSeconds(),{unit:"second"}):nf.s(t,e)},S:function(t,e){return nf.S(t,e)},X:function(t,e,i){const o=t.getTimezoneOffset();if(0===o)return"Z";switch(e){case"X":return mf(o);case"XXXX":case"XX":return pf(o);default:return pf(o,":")}},x:function(t,e,i){const o=t.getTimezoneOffset();switch(e){case"x":return mf(o);case"xxxx":case"xx":return pf(o);default:return pf(o,":")}},O:function(t,e,i){const o=t.getTimezoneOffset();switch(e){case"O":case"OO":case"OOO":return"GMT"+uf(o,":");default:return"GMT"+pf(o,":")}},z:function(t,e,i){const o=t.getTimezoneOffset();switch(e){case"z":case"zz":case"zzz":return"GMT"+uf(o,":");default:return"GMT"+pf(o,":")}},t:function(t,e,i){return of(Math.trunc(t.getTime()/1e3),e.length)},T:function(t,e,i){return of(t.getTime(),e.length)}};function uf(t,e=""){const i=t>0?"-":"+",o=Math.abs(t),n=Math.trunc(o/60),a=o%60;return 0===a?i+String(n):i+String(n)+e+of(a,2)}function mf(t,e){if(t%60==0){return(t>0?"-":"+")+of(Math.abs(t)/60,2)}return pf(t,e)}function pf(t,e=""){const i=t>0?"-":"+",o=Math.abs(t);return i+of(Math.trunc(o/60),2)+e+of(o%60,2)}const _f=(t,e)=>{switch(t){case"P":return e.date({width:"short"});case"PP":return e.date({width:"medium"});case"PPP":return e.date({width:"long"});default:return e.date({width:"full"})}},gf=(t,e)=>{switch(t){case"p":return e.time({width:"short"});case"pp":return e.time({width:"medium"});case"ppp":return e.time({width:"long"});default:return e.time({width:"full"})}},ff={p:gf,P:(t,e)=>{const i=t.match(/(P+)(p+)?/)||[],o=i[1],n=i[2];if(!n)return _f(t,e);let a;switch(o){case"P":a=e.dateTime({width:"short"});break;case"PP":a=e.dateTime({width:"medium"});break;case"PPP":a=e.dateTime({width:"long"});break;default:a=e.dateTime({width:"full"})}return a.replace("{{date}}",_f(o,e)).replace("{{time}}",gf(n,e))}},bf=/^D+$/,vf=/^Y+$/,yf=["D","DD","YY","YYYY"];function wf(t){return bf.test(t)}function xf(t){return vf.test(t)}function kf(t,e,i){const o=function(t,e,i){const o="Y"===t[0]?"years":"days of the month";return`Use \`${t.toLowerCase()}\` instead of \`${t}\` (in \`${e}\`) for formatting ${o} to the input \`${i}\`; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md`}(t,e,i);if(console.warn(o),yf.includes(t))throw new RangeError(o)}const Tf=/[yYQqMLwIdDecihHKkms]o|(\w)\1*|''|'(''|[^'])+('|$)|./g,Sf=/P+p+|P+|p+|''|'(''|[^'])+('|$)|./g,Mf=/^'([^]*?)'?$/,Df=/''/g,Cf=/[a-zA-Z]/;function Af(t){const e=t.match(Mf);return e?e[1].replace(Df,"'"):t}class Pf{subPriority=0;validate(t,e){return!0}}class Ef extends Pf{constructor(t,e,i,o,n){super(),this.value=t,this.validateValue=e,this.setValue=i,this.priority=o,n&&(this.subPriority=n)}validate(t,e){return this.validateValue(t,this.value,e)}set(t,e,i){return this.setValue(t,e,this.value,i)}}class Lf extends Pf{priority=10;subPriority=-1;set(t,e){return e.timestampIsSet?t:gg(t,function(t,e){const i=e instanceof Date?gg(e,0):new e(0);return i.setFullYear(t.getFullYear(),t.getMonth(),t.getDate()),i.setHours(t.getHours(),t.getMinutes(),t.getSeconds(),t.getMilliseconds()),i}(t,Date))}}class $f{run(t,e,i,o){const n=this.parse(t,e,i,o);return n?{setter:new Ef(n.value,this.validate,this.set,this.priority,this.subPriority),rest:n.rest}:null}validate(t,e,i){return!0}}const zf=/^(1[0-2]|0?\d)/,Of=/^(3[0-1]|[0-2]?\d)/,Vf=/^(36[0-6]|3[0-5]\d|[0-2]?\d?\d)/,Hf=/^(5[0-3]|[0-4]?\d)/,Rf=/^(2[0-3]|[0-1]?\d)/,If=/^(2[0-4]|[0-1]?\d)/,Nf=/^(1[0-1]|0?\d)/,Ff=/^(1[0-2]|0?\d)/,Wf=/^[0-5]?\d/,Bf=/^[0-5]?\d/,jf=/^\d/,Uf=/^\d{1,2}/,Yf=/^\d{1,3}/,qf=/^\d{1,4}/,Zf=/^-?\d+/,Xf=/^-?\d/,Kf=/^-?\d{1,2}/,Gf=/^-?\d{1,3}/,Qf=/^-?\d{1,4}/,Jf=/^([+-])(\d{2})(\d{2})?|Z/,tb=/^([+-])(\d{2})(\d{2})|Z/,eb=/^([+-])(\d{2})(\d{2})((\d{2}))?|Z/,ib=/^([+-])(\d{2}):(\d{2})|Z/,ob=/^([+-])(\d{2}):(\d{2})(:(\d{2}))?|Z/;function nb(t,e){return t?{value:e(t.value),rest:t.rest}:t}function ab(t,e){const i=e.match(t);return i?{value:parseInt(i[0],10),rest:e.slice(i[0].length)}:null}function sb(t,e){const i=e.match(t);if(!i)return null;if("Z"===i[0])return{value:0,rest:e.slice(1)};const o="+"===i[1]?1:-1,n=i[2]?parseInt(i[2],10):0,a=i[3]?parseInt(i[3],10):0,s=i[5]?parseInt(i[5],10):0;return{value:o*(n*kg+a*xg+1e3*s),rest:e.slice(i[0].length)}}function rb(t){return ab(Zf,t)}function lb(t,e){switch(t){case 1:return ab(jf,e);case 2:return ab(Uf,e);case 3:return ab(Yf,e);case 4:return ab(qf,e);default:return ab(new RegExp("^\\d{1,"+t+"}"),e)}}function cb(t,e){switch(t){case 1:return ab(Xf,e);case 2:return ab(Kf,e);case 3:return ab(Gf,e);case 4:return ab(Qf,e);default:return ab(new RegExp("^-?\\d{1,"+t+"}"),e)}}function db(t){switch(t){case"morning":return 4;case"evening":return 17;case"pm":case"noon":case"afternoon":return 12;default:return 0}}function hb(t,e){const i=e>0,o=i?e:1-e;let n;if(o<=50)n=t||100;else{const e=o+50;n=t+100*Math.trunc(e/100)-(t>=e%100?100:0)}return i?n:1-n}function ub(t){return t%400==0||t%4==0&&t%100!=0}const mb=[31,28,31,30,31,30,31,31,30,31,30,31],pb=[31,29,31,30,31,30,31,31,30,31,30,31];function _b(t,e,i){const o=Sg(),n=i?.weekStartsOn??i?.locale?.options?.weekStartsOn??o.weekStartsOn??o.locale?.options?.weekStartsOn??0,a=_g(t),s=a.getDay(),r=7-n;return fg(a,e<0||e>6?e-(s+r)%7:((e%7+7)%7+r)%7-(s+r)%7)}function gb(t,e){const i=_g(t),o=function(t){let e=_g(t).getDay();return 0===e&&(e=7),e}(i);return fg(i,e-o)}const fb={G:new class extends $f{priority=140;parse(t,e,i){switch(e){case"G":case"GG":case"GGG":return i.era(t,{width:"abbreviated"})||i.era(t,{width:"narrow"});case"GGGGG":return i.era(t,{width:"narrow"});default:return i.era(t,{width:"wide"})||i.era(t,{width:"abbreviated"})||i.era(t,{width:"narrow"})}}set(t,e,i){return e.era=i,t.setFullYear(i,0,1),t.setHours(0,0,0,0),t}incompatibleTokens=["R","u","t","T"]},y:new class extends $f{priority=130;incompatibleTokens=["Y","R","u","w","I","i","e","c","t","T"];parse(t,e,i){const o=t=>({year:t,isTwoDigitYear:"yy"===e});switch(e){case"y":return nb(lb(4,t),o);case"yo":return nb(i.ordinalNumber(t,{unit:"year"}),o);default:return nb(lb(e.length,t),o)}}validate(t,e){return e.isTwoDigitYear||e.year>0}set(t,e,i){const o=t.getFullYear();if(i.isTwoDigitYear){const e=hb(i.year,o);return t.setFullYear(e,0,1),t.setHours(0,0,0,0),t}const n="era"in e&&1!==e.era?1-i.year:i.year;return t.setFullYear(n,0,1),t.setHours(0,0,0,0),t}},Y:new class extends $f{priority=130;parse(t,e,i){const o=t=>({year:t,isTwoDigitYear:"YY"===e});switch(e){case"Y":return nb(lb(4,t),o);case"Yo":return nb(i.ordinalNumber(t,{unit:"year"}),o);default:return nb(lb(e.length,t),o)}}validate(t,e){return e.isTwoDigitYear||e.year>0}set(t,e,i,o){const n=tf(t,o);if(i.isTwoDigitYear){const e=hb(i.year,n);return t.setFullYear(e,0,o.firstWeekContainsDate),t.setHours(0,0,0,0),Mg(t,o)}const a="era"in e&&1!==e.era?1-i.year:i.year;return t.setFullYear(a,0,o.firstWeekContainsDate),t.setHours(0,0,0,0),Mg(t,o)}incompatibleTokens=["y","R","u","Q","q","M","L","I","d","D","i","t","T"]},R:new class extends $f{priority=130;parse(t,e){return cb("R"===e?4:e.length,t)}set(t,e,i){const o=gg(t,0);return o.setFullYear(i,0,4),o.setHours(0,0,0,0),Dg(o)}incompatibleTokens=["G","y","Y","u","Q","q","M","L","w","d","D","e","c","t","T"]},u:new class extends $f{priority=130;parse(t,e){return cb("u"===e?4:e.length,t)}set(t,e,i){return t.setFullYear(i,0,1),t.setHours(0,0,0,0),t}incompatibleTokens=["G","y","Y","R","w","I","i","e","c","t","T"]},Q:new class extends $f{priority=120;parse(t,e,i){switch(e){case"Q":case"QQ":return lb(e.length,t);case"Qo":return i.ordinalNumber(t,{unit:"quarter"});case"QQQ":return i.quarter(t,{width:"abbreviated",context:"formatting"})||i.quarter(t,{width:"narrow",context:"formatting"});case"QQQQQ":return i.quarter(t,{width:"narrow",context:"formatting"});default:return i.quarter(t,{width:"wide",context:"formatting"})||i.quarter(t,{width:"abbreviated",context:"formatting"})||i.quarter(t,{width:"narrow",context:"formatting"})}}validate(t,e){return e>=1&&e<=4}set(t,e,i){return t.setMonth(3*(i-1),1),t.setHours(0,0,0,0),t}incompatibleTokens=["Y","R","q","M","L","w","I","d","D","i","e","c","t","T"]},q:new class extends $f{priority=120;parse(t,e,i){switch(e){case"q":case"qq":return lb(e.length,t);case"qo":return i.ordinalNumber(t,{unit:"quarter"});case"qqq":return i.quarter(t,{width:"abbreviated",context:"standalone"})||i.quarter(t,{width:"narrow",context:"standalone"});case"qqqqq":return i.quarter(t,{width:"narrow",context:"standalone"});default:return i.quarter(t,{width:"wide",context:"standalone"})||i.quarter(t,{width:"abbreviated",context:"standalone"})||i.quarter(t,{width:"narrow",context:"standalone"})}}validate(t,e){return e>=1&&e<=4}set(t,e,i){return t.setMonth(3*(i-1),1),t.setHours(0,0,0,0),t}incompatibleTokens=["Y","R","Q","M","L","w","I","d","D","i","e","c","t","T"]},M:new class extends $f{incompatibleTokens=["Y","R","q","Q","L","w","I","D","i","e","c","t","T"];priority=110;parse(t,e,i){const o=t=>t-1;switch(e){case"M":return nb(ab(zf,t),o);case"MM":return nb(lb(2,t),o);case"Mo":return nb(i.ordinalNumber(t,{unit:"month"}),o);case"MMM":return i.month(t,{width:"abbreviated",context:"formatting"})||i.month(t,{width:"narrow",context:"formatting"});case"MMMMM":return i.month(t,{width:"narrow",context:"formatting"});default:return i.month(t,{width:"wide",context:"formatting"})||i.month(t,{width:"abbreviated",context:"formatting"})||i.month(t,{width:"narrow",context:"formatting"})}}validate(t,e){return e>=0&&e<=11}set(t,e,i){return t.setMonth(i,1),t.setHours(0,0,0,0),t}},L:new class extends $f{priority=110;parse(t,e,i){const o=t=>t-1;switch(e){case"L":return nb(ab(zf,t),o);case"LL":return nb(lb(2,t),o);case"Lo":return nb(i.ordinalNumber(t,{unit:"month"}),o);case"LLL":return i.month(t,{width:"abbreviated",context:"standalone"})||i.month(t,{width:"narrow",context:"standalone"});case"LLLLL":return i.month(t,{width:"narrow",context:"standalone"});default:return i.month(t,{width:"wide",context:"standalone"})||i.month(t,{width:"abbreviated",context:"standalone"})||i.month(t,{width:"narrow",context:"standalone"})}}validate(t,e){return e>=0&&e<=11}set(t,e,i){return t.setMonth(i,1),t.setHours(0,0,0,0),t}incompatibleTokens=["Y","R","q","Q","M","w","I","D","i","e","c","t","T"]},w:new class extends $f{priority=100;parse(t,e,i){switch(e){case"w":return ab(Hf,t);case"wo":return i.ordinalNumber(t,{unit:"week"});default:return lb(e.length,t)}}validate(t,e){return e>=1&&e<=53}set(t,e,i,o){return Mg(function(t,e,i){const o=_g(t),n=ef(o,i)-e;return o.setDate(o.getDate()-7*n),o}(t,i,o),o)}incompatibleTokens=["y","R","u","q","Q","M","L","I","d","D","i","t","T"]},I:new class extends $f{priority=100;parse(t,e,i){switch(e){case"I":return ab(Hf,t);case"Io":return i.ordinalNumber(t,{unit:"week"});default:return lb(e.length,t)}}validate(t,e){return e>=1&&e<=53}set(t,e,i){return Dg(function(t,e){const i=_g(t),o=Jg(i)-e;return i.setDate(i.getDate()-7*o),i}(t,i))}incompatibleTokens=["y","Y","u","q","Q","M","L","w","d","D","e","c","t","T"]},d:new class extends $f{priority=90;subPriority=1;parse(t,e,i){switch(e){case"d":return ab(Of,t);case"do":return i.ordinalNumber(t,{unit:"date"});default:return lb(e.length,t)}}validate(t,e){const i=ub(t.getFullYear()),o=t.getMonth();return i?e>=1&&e<=pb[o]:e>=1&&e<=mb[o]}set(t,e,i){return t.setDate(i),t.setHours(0,0,0,0),t}incompatibleTokens=["Y","R","q","Q","w","I","D","i","e","c","t","T"]},D:new class extends $f{priority=90;subpriority=1;parse(t,e,i){switch(e){case"D":case"DD":return ab(Vf,t);case"Do":return i.ordinalNumber(t,{unit:"date"});default:return lb(e.length,t)}}validate(t,e){return ub(t.getFullYear())?e>=1&&e<=366:e>=1&&e<=365}set(t,e,i){return t.setMonth(0,i),t.setHours(0,0,0,0),t}incompatibleTokens=["Y","R","q","Q","M","L","w","I","d","E","i","e","c","t","T"]},E:new class extends $f{priority=90;parse(t,e,i){switch(e){case"E":case"EE":case"EEE":return i.day(t,{width:"abbreviated",context:"formatting"})||i.day(t,{width:"short",context:"formatting"})||i.day(t,{width:"narrow",context:"formatting"});case"EEEEE":return i.day(t,{width:"narrow",context:"formatting"});case"EEEEEE":return i.day(t,{width:"short",context:"formatting"})||i.day(t,{width:"narrow",context:"formatting"});default:return i.day(t,{width:"wide",context:"formatting"})||i.day(t,{width:"abbreviated",context:"formatting"})||i.day(t,{width:"short",context:"formatting"})||i.day(t,{width:"narrow",context:"formatting"})}}validate(t,e){return e>=0&&e<=6}set(t,e,i,o){return(t=_b(t,i,o)).setHours(0,0,0,0),t}incompatibleTokens=["D","i","e","c","t","T"]},e:new class extends $f{priority=90;parse(t,e,i,o){const n=t=>{const e=7*Math.floor((t-1)/7);return(t+o.weekStartsOn+6)%7+e};switch(e){case"e":case"ee":return nb(lb(e.length,t),n);case"eo":return nb(i.ordinalNumber(t,{unit:"day"}),n);case"eee":return i.day(t,{width:"abbreviated",context:"formatting"})||i.day(t,{width:"short",context:"formatting"})||i.day(t,{width:"narrow",context:"formatting"});case"eeeee":return i.day(t,{width:"narrow",context:"formatting"});case"eeeeee":return i.day(t,{width:"short",context:"formatting"})||i.day(t,{width:"narrow",context:"formatting"});default:return i.day(t,{width:"wide",context:"formatting"})||i.day(t,{width:"abbreviated",context:"formatting"})||i.day(t,{width:"short",context:"formatting"})||i.day(t,{width:"narrow",context:"formatting"})}}validate(t,e){return e>=0&&e<=6}set(t,e,i,o){return(t=_b(t,i,o)).setHours(0,0,0,0),t}incompatibleTokens=["y","R","u","q","Q","M","L","I","d","D","E","i","c","t","T"]},c:new class extends $f{priority=90;parse(t,e,i,o){const n=t=>{const e=7*Math.floor((t-1)/7);return(t+o.weekStartsOn+6)%7+e};switch(e){case"c":case"cc":return nb(lb(e.length,t),n);case"co":return nb(i.ordinalNumber(t,{unit:"day"}),n);case"ccc":return i.day(t,{width:"abbreviated",context:"standalone"})||i.day(t,{width:"short",context:"standalone"})||i.day(t,{width:"narrow",context:"standalone"});case"ccccc":return i.day(t,{width:"narrow",context:"standalone"});case"cccccc":return i.day(t,{width:"short",context:"standalone"})||i.day(t,{width:"narrow",context:"standalone"});default:return i.day(t,{width:"wide",context:"standalone"})||i.day(t,{width:"abbreviated",context:"standalone"})||i.day(t,{width:"short",context:"standalone"})||i.day(t,{width:"narrow",context:"standalone"})}}validate(t,e){return e>=0&&e<=6}set(t,e,i,o){return(t=_b(t,i,o)).setHours(0,0,0,0),t}incompatibleTokens=["y","R","u","q","Q","M","L","I","d","D","E","i","e","t","T"]},i:new class extends $f{priority=90;parse(t,e,i){const o=t=>0===t?7:t;switch(e){case"i":case"ii":return lb(e.length,t);case"io":return i.ordinalNumber(t,{unit:"day"});case"iii":return nb(i.day(t,{width:"abbreviated",context:"formatting"})||i.day(t,{width:"short",context:"formatting"})||i.day(t,{width:"narrow",context:"formatting"}),o);case"iiiii":return nb(i.day(t,{width:"narrow",context:"formatting"}),o);case"iiiiii":return nb(i.day(t,{width:"short",context:"formatting"})||i.day(t,{width:"narrow",context:"formatting"}),o);default:return nb(i.day(t,{width:"wide",context:"formatting"})||i.day(t,{width:"abbreviated",context:"formatting"})||i.day(t,{width:"short",context:"formatting"})||i.day(t,{width:"narrow",context:"formatting"}),o)}}validate(t,e){return e>=1&&e<=7}set(t,e,i){return(t=gb(t,i)).setHours(0,0,0,0),t}incompatibleTokens=["y","Y","u","q","Q","M","L","w","d","D","E","e","c","t","T"]},a:new class extends $f{priority=80;parse(t,e,i){switch(e){case"a":case"aa":case"aaa":return i.dayPeriod(t,{width:"abbreviated",context:"formatting"})||i.dayPeriod(t,{width:"narrow",context:"formatting"});case"aaaaa":return i.dayPeriod(t,{width:"narrow",context:"formatting"});default:return i.dayPeriod(t,{width:"wide",context:"formatting"})||i.dayPeriod(t,{width:"abbreviated",context:"formatting"})||i.dayPeriod(t,{width:"narrow",context:"formatting"})}}set(t,e,i){return t.setHours(db(i),0,0,0),t}incompatibleTokens=["b","B","H","k","t","T"]},b:new class extends $f{priority=80;parse(t,e,i){switch(e){case"b":case"bb":case"bbb":return i.dayPeriod(t,{width:"abbreviated",context:"formatting"})||i.dayPeriod(t,{width:"narrow",context:"formatting"});case"bbbbb":return i.dayPeriod(t,{width:"narrow",context:"formatting"});default:return i.dayPeriod(t,{width:"wide",context:"formatting"})||i.dayPeriod(t,{width:"abbreviated",context:"formatting"})||i.dayPeriod(t,{width:"narrow",context:"formatting"})}}set(t,e,i){return t.setHours(db(i),0,0,0),t}incompatibleTokens=["a","B","H","k","t","T"]},B:new class extends $f{priority=80;parse(t,e,i){switch(e){case"B":case"BB":case"BBB":return i.dayPeriod(t,{width:"abbreviated",context:"formatting"})||i.dayPeriod(t,{width:"narrow",context:"formatting"});case"BBBBB":return i.dayPeriod(t,{width:"narrow",context:"formatting"});default:return i.dayPeriod(t,{width:"wide",context:"formatting"})||i.dayPeriod(t,{width:"abbreviated",context:"formatting"})||i.dayPeriod(t,{width:"narrow",context:"formatting"})}}set(t,e,i){return t.setHours(db(i),0,0,0),t}incompatibleTokens=["a","b","t","T"]},h:new class extends $f{priority=70;parse(t,e,i){switch(e){case"h":return ab(Ff,t);case"ho":return i.ordinalNumber(t,{unit:"hour"});default:return lb(e.length,t)}}validate(t,e){return e>=1&&e<=12}set(t,e,i){const o=t.getHours()>=12;return o&&i<12?t.setHours(i+12,0,0,0):o||12!==i?t.setHours(i,0,0,0):t.setHours(0,0,0,0),t}incompatibleTokens=["H","K","k","t","T"]},H:new class extends $f{priority=70;parse(t,e,i){switch(e){case"H":return ab(Rf,t);case"Ho":return i.ordinalNumber(t,{unit:"hour"});default:return lb(e.length,t)}}validate(t,e){return e>=0&&e<=23}set(t,e,i){return t.setHours(i,0,0,0),t}incompatibleTokens=["a","b","h","K","k","t","T"]},K:new class extends $f{priority=70;parse(t,e,i){switch(e){case"K":return ab(Nf,t);case"Ko":return i.ordinalNumber(t,{unit:"hour"});default:return lb(e.length,t)}}validate(t,e){return e>=0&&e<=11}set(t,e,i){return t.getHours()>=12&&i<12?t.setHours(i+12,0,0,0):t.setHours(i,0,0,0),t}incompatibleTokens=["h","H","k","t","T"]},k:new class extends $f{priority=70;parse(t,e,i){switch(e){case"k":return ab(If,t);case"ko":return i.ordinalNumber(t,{unit:"hour"});default:return lb(e.length,t)}}validate(t,e){return e>=1&&e<=24}set(t,e,i){const o=i<=24?i%24:i;return t.setHours(o,0,0,0),t}incompatibleTokens=["a","b","h","H","K","t","T"]},m:new class extends $f{priority=60;parse(t,e,i){switch(e){case"m":return ab(Wf,t);case"mo":return i.ordinalNumber(t,{unit:"minute"});default:return lb(e.length,t)}}validate(t,e){return e>=0&&e<=59}set(t,e,i){return t.setMinutes(i,0,0),t}incompatibleTokens=["t","T"]},s:new class extends $f{priority=50;parse(t,e,i){switch(e){case"s":return ab(Bf,t);case"so":return i.ordinalNumber(t,{unit:"second"});default:return lb(e.length,t)}}validate(t,e){return e>=0&&e<=59}set(t,e,i){return t.setSeconds(i,0),t}incompatibleTokens=["t","T"]},S:new class extends $f{priority=30;parse(t,e){return nb(lb(e.length,t),(t=>Math.trunc(t*Math.pow(10,3-e.length))))}set(t,e,i){return t.setMilliseconds(i),t}incompatibleTokens=["t","T"]},X:new class extends $f{priority=10;parse(t,e){switch(e){case"X":return sb(Jf,t);case"XX":return sb(tb,t);case"XXXX":return sb(eb,t);case"XXXXX":return sb(ob,t);default:return sb(ib,t)}}set(t,e,i){return e.timestampIsSet?t:gg(t,t.getTime()-Pg(t)-i)}incompatibleTokens=["t","T","x"]},x:new class extends $f{priority=10;parse(t,e){switch(e){case"x":return sb(Jf,t);case"xx":return sb(tb,t);case"xxxx":return sb(eb,t);case"xxxxx":return sb(ob,t);default:return sb(ib,t)}}set(t,e,i){return e.timestampIsSet?t:gg(t,t.getTime()-Pg(t)-i)}incompatibleTokens=["t","T","X"]},t:new class extends $f{priority=40;parse(t){return rb(t)}set(t,e,i){return[gg(t,1e3*i),{timestampIsSet:!0}]}incompatibleTokens="*"},T:new class extends $f{priority=20;parse(t){return rb(t)}set(t,e,i){return[gg(t,i),{timestampIsSet:!0}]}incompatibleTokens="*"}},bb=/[yYQqMLwIdDecihHKkms]o|(\w)\1*|''|'(''|[^'])+('|$)|./g,vb=/P+p+|P+|p+|''|'(''|[^'])+('|$)|./g,yb=/^'([^]*?)'?$/,wb=/''/g,xb=/\S/,kb=/[a-zA-Z]/;function Tb(t,e,i,o){const n=Object.assign({},Sg()),a=o?.locale??n.locale??Qg,s=o?.firstWeekContainsDate??o?.locale?.options?.firstWeekContainsDate??n.firstWeekContainsDate??n.locale?.options?.firstWeekContainsDate??1,r=o?.weekStartsOn??o?.locale?.options?.weekStartsOn??n.weekStartsOn??n.locale?.options?.weekStartsOn??0;if(""===e)return""===t?_g(i):gg(i,NaN);const l={firstWeekContainsDate:s,weekStartsOn:r,locale:a},c=[new Lf],d=e.match(vb).map((t=>{const e=t[0];if(e in ff){return(0,ff[e])(t,a.formatLong)}return t})).join("").match(bb),h=[];for(let n of d){!o?.useAdditionalWeekYearTokens&&xf(n)&&kf(n,e,t),!o?.useAdditionalDayOfYearTokens&&wf(n)&&kf(n,e,t);const s=n[0],r=fb[s];if(r){const{incompatibleTokens:e}=r;if(Array.isArray(e)){const t=h.find((t=>e.includes(t.token)||t.token===s));if(t)throw new RangeError(`The format string mustn't contain \`${t.fullToken}\` and \`${n}\` at the same time`)}else if("*"===r.incompatibleTokens&&h.length>0)throw new RangeError(`The format string mustn't contain \`${n}\` and any other token at the same time`);h.push({token:s,fullToken:n});const o=r.run(t,n,a.match,l);if(!o)return gg(i,NaN);c.push(o.setter),t=o.rest}else{if(s.match(kb))throw new RangeError("Format string contains an unescaped latin alphabet character `"+s+"`");if("''"===n?n="'":"'"===s&&(n=n.match(yb)[1].replace(wb,"'")),0!==t.indexOf(n))return gg(i,NaN);t=t.slice(n.length)}}if(t.length>0&&xb.test(t))return gg(i,NaN);const u=c.map((t=>t.priority)).sort(((t,e)=>e-t)).filter(((t,e,i)=>i.indexOf(t)===e)).map((t=>c.filter((e=>e.priority===t)).sort(((t,e)=>e.subPriority-t.subPriority)))).map((t=>t[0]));let m=_g(i);if(isNaN(m.getTime()))return gg(i,NaN);const p={};for(const t of u){if(!t.validate(m,l))return gg(i,NaN);const e=t.set(m,p,l);Array.isArray(e)?(m=e[0],Object.assign(p,e[1])):m=e}return gg(i,m)}function Sb(t,e){const i=e?.additionalDigits??2,o=function(t){const e={},i=t.split(Mb.dateTimeDelimiter);let o;if(i.length>2)return e;/:/.test(i[0])?o=i[0]:(e.date=i[0],o=i[1],Mb.timeZoneDelimiter.test(e.date)&&(e.date=t.split(Mb.timeZoneDelimiter)[0],o=t.substr(e.date.length,t.length)));if(o){const t=Mb.timezone.exec(o);t?(e.time=o.replace(t[1],""),e.timezone=t[1]):e.time=o}return e}(t);let n;if(o.date){const t=function(t,e){const i=new RegExp("^(?:(\\d{4}|[+-]\\d{"+(4+e)+"})|(\\d{2}|[+-]\\d{"+(2+e)+"})$)"),o=t.match(i);if(!o)return{year:NaN,restDateString:""};const n=o[1]?parseInt(o[1]):null,a=o[2]?parseInt(o[2]):null;return{year:null===a?n:100*a,restDateString:t.slice((o[1]||o[2]).length)}}(o.date,i);n=function(t,e){if(null===e)return new Date(NaN);const i=t.match(Db);if(!i)return new Date(NaN);const o=!!i[4],n=Pb(i[1]),a=Pb(i[2])-1,s=Pb(i[3]),r=Pb(i[4]),l=Pb(i[5])-1;if(o)return function(t,e,i){return e>=1&&e<=53&&i>=0&&i<=6}(0,r,l)?function(t,e,i){const o=new Date(0);o.setUTCFullYear(t,0,4);const n=o.getUTCDay()||7,a=7*(e-1)+i+1-n;return o.setUTCDate(o.getUTCDate()+a),o}(e,r,l):new Date(NaN);{const t=new Date(0);return function(t,e,i){return e>=0&&e<=11&&i>=1&&i<=(Lb[e]||($b(t)?29:28))}(e,a,s)&&function(t,e){return e>=1&&e<=($b(t)?366:365)}(e,n)?(t.setUTCFullYear(e,a,Math.max(n,s)),t):new Date(NaN)}}(t.restDateString,t.year)}if(!n||isNaN(n.getTime()))return new Date(NaN);const a=n.getTime();let s,r=0;if(o.time&&(r=function(t){const e=t.match(Cb);if(!e)return NaN;const i=Eb(e[1]),o=Eb(e[2]),n=Eb(e[3]);if(!function(t,e,i){if(24===t)return 0===e&&0===i;return i>=0&&i<60&&e>=0&&e<60&&t>=0&&t<25}(i,o,n))return NaN;return i*kg+o*xg+1e3*n}(o.time),isNaN(r)))return new Date(NaN);if(!o.timezone){const t=new Date(a+r),e=new Date(0);return e.setFullYear(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate()),e.setHours(t.getUTCHours(),t.getUTCMinutes(),t.getUTCSeconds(),t.getUTCMilliseconds()),e}return s=function(t){if("Z"===t)return 0;const e=t.match(Ab);if(!e)return 0;const i="+"===e[1]?-1:1,o=parseInt(e[2]),n=e[3]&&parseInt(e[3])||0;if(!function(t,e){return e>=0&&e<=59}(0,n))return NaN;return i*(o*kg+n*xg)}(o.timezone),isNaN(s)?new Date(NaN):new Date(a+r+s)}const Mb={dateTimeDelimiter:/[T ]/,timeZoneDelimiter:/[Z ]/i,timezone:/([Z+-].*)$/},Db=/^-?(?:(\d{3})|(\d{2})(?:-?(\d{2}))?|W(\d{2})(?:-?(\d{1}))?|)$/,Cb=/^(\d{2}(?:[.,]\d*)?)(?::?(\d{2}(?:[.,]\d*)?))?(?::?(\d{2}(?:[.,]\d*)?))?$/,Ab=/^([+-])(\d{2})(?::?(\d{2}))?$/;function Pb(t){return t?parseInt(t):1}function Eb(t){return t&&parseFloat(t.replace(",","."))||0}const Lb=[31,null,31,30,31,30,31,31,30,31,30,31];function $b(t){return t%400==0||t%4==0&&t%100!=0}const zb={datetime:"MMM d, yyyy, h:mm:ss aaaa",millisecond:"h:mm:ss.SSS aaaa",second:"h:mm:ss aaaa",minute:"h:mm aaaa",hour:"ha",day:"MMM d",week:"PP",month:"MMM yyyy",quarter:"qqq - yyyy",year:"yyyy"};Lh._date.override({_id:"date-fns",formats:function(){return zb},parse:function(t,e){if(null==t)return null;const i=typeof t;return"number"===i||t instanceof Date?t=_g(t):"string"===i&&(t="string"==typeof e?Tb(t,e,new Date,this.options):Sb(t,this.options)),$g(t)?t.getTime():null},format:function(t,e){return function(t,e,i){const o=Sg(),n=i?.locale??o.locale??Qg,a=i?.firstWeekContainsDate??i?.locale?.options?.firstWeekContainsDate??o.firstWeekContainsDate??o.locale?.options?.firstWeekContainsDate??1,s=i?.weekStartsOn??i?.locale?.options?.weekStartsOn??o.weekStartsOn??o.locale?.options?.weekStartsOn??0,r=_g(t);if(!$g(r))throw new RangeError("Invalid time value");let l=e.match(Sf).map((t=>{const e=t[0];return"p"===e||"P"===e?(0,ff[e])(t,n.formatLong):t})).join("").match(Tf).map((t=>{if("''"===t)return{isToken:!1,value:"'"};const e=t[0];if("'"===e)return{isToken:!1,value:Af(t)};if(hf[e])return{isToken:!0,value:t};if(e.match(Cf))throw new RangeError("Format string contains an unescaped latin alphabet character `"+e+"`");return{isToken:!1,value:t}}));n.localize.preprocessor&&(l=n.localize.preprocessor(r,l));const c={firstWeekContainsDate:a,weekStartsOn:s,locale:n};return l.map((o=>{if(!o.isToken)return o.value;const a=o.value;return(!i?.useAdditionalWeekYearTokens&&xf(a)||!i?.useAdditionalDayOfYearTokens&&wf(a))&&kf(a,e,String(t)),(0,hf[a[0]])(r,a,n.localize,c)})).join("")}(t,e,this.options)},add:function(t,e,i){switch(i){case"millisecond":return vg(t,e);case"second":return function(t,e){return vg(t,1e3*e)}(t,e);case"minute":return function(t,e){return vg(t,e*xg)}(t,e);case"hour":return function(t,e){return vg(t,e*kg)}(t,e);case"day":return fg(t,e);case"week":return function(t,e){return fg(t,7*e)}(t,e);case"month":return bg(t,e);case"quarter":return function(t,e){return bg(t,3*e)}(t,e);case"year":return function(t,e){return bg(t,12*e)}(t,e);default:return t}},diff:function(t,e,i){switch(i){case"millisecond":return Hg(t,e);case"second":return function(t,e,i){const o=Hg(t,e)/1e3;return Vg(i?.roundingMethod)(o)}(t,e);case"minute":return function(t,e,i){const o=Hg(t,e)/xg;return Vg(i?.roundingMethod)(o)}(t,e);case"hour":return function(t,e,i){const o=Hg(t,e)/kg;return Vg(i?.roundingMethod)(o)}(t,e);case"day":return zg(t,e);case"week":return function(t,e,i){const o=zg(t,e)/7;return Vg(i?.roundingMethod)(o)}(t,e);case"month":return Ng(t,e);case"quarter":return function(t,e,i){const o=Ng(t,e)/3;return Vg(i?.roundingMethod)(o)}(t,e);case"year":return Fg(t,e);default:return 0}},startOf:function(t,e,i){switch(e){case"second":return function(t){const e=_g(t);return e.setMilliseconds(0),e}(t);case"minute":return function(t){const e=_g(t);return e.setSeconds(0,0),e}(t);case"hour":return function(t){const e=_g(t);return e.setMinutes(0,0,0),e}(t);case"day":return Ag(t);case"week":return Mg(t);case"isoWeek":return Mg(t,{weekStartsOn:+i});case"month":return function(t){const e=_g(t);return e.setDate(1),e.setHours(0,0,0,0),e}(t);case"quarter":return function(t){const e=_g(t),i=e.getMonth(),o=i-i%3;return e.setMonth(o,1),e.setHours(0,0,0,0),e}(t);case"year":return Wg(t);default:return t}},endOf:function(t,e){switch(e){case"second":return function(t){const e=_g(t);return e.setMilliseconds(999),e}(t);case"minute":return function(t){const e=_g(t);return e.setSeconds(59,999),e}(t);case"hour":return function(t){const e=_g(t);return e.setMinutes(59,59,999),e}(t);case"day":return Rg(t);case"week":return function(t,e){const i=Sg(),o=e?.weekStartsOn??e?.locale?.options?.weekStartsOn??i.weekStartsOn??i.locale?.options?.weekStartsOn??0,n=_g(t),a=n.getDay(),s=6+(at.schema)),o=Object.assign({},...i);return e?Yn(o):Bn(o)}(Bn({index:jn(Wn()),view_index:jn(Wn()),view_layout:Rn(),type:Un()}),Bn({entity:jn(Un()),name:jn(Un()),icon:jn(Un()),autoStartStopEnableEntity:jn(Un()),powerEntity:jn(Un()),visibility:jn(Rn())}),Bn({disable_name:jn(Nn()),disable_window:jn(Nn()),disable_autoStartStop:jn(Nn()),disable_overpowering:jn(Nn()),disable_heat:jn(Nn()),disable_cool:jn(Nn()),disable_heat_cool:jn(Nn()),disable_auto:jn(Nn()),disable_dry:jn(Nn()),disable_fan_only:jn(Nn()),disable_off:jn(Nn()),disable_sleep:jn(Nn()),disable_safety_warning:jn(Nn()),disable_power_infos:jn(Nn()),disable_auto_fan_infos:jn(Nn()),disable_target_icon:jn(Nn()),set_current_as_main:jn(Nn()),eco_temperature:jn(Wn()),disable_menu:jn(Nn()),disable_buttons:jn(Nn()),allow_lock_toggle:jn(Nn()),disable_presets:jn(Nn()),theme:jn(qn([Fn("classic"),Fn("vtherm"),Fn("uncolored"),Fn("gunmalmg")])),disable_timed_preset:jn(Nn()),use_manual_duration_input:jn(Nn()),allow_preset_modification:jn(Nn()),show_status_text:jn(Nn()),show_background_gradient_on_active:jn(Nn()),show_regulation_chart:jn(Nn())}));var Hb;const Rb=["icon_color","layout","fill_container","primary_info","secondary_info","icon_type","content_info","use_entity_picture","collapsible_controls","icon_animation"],Ib=t=>{var e,i;customElements.get("ha-form")&&(customElements.get("hui-action-editor")||((t,e,i,o)=>{const[n,a,s]=t.split(".",3);return Number(n)>e||Number(n)===e&&(void 0===o?Number(a)>=i:Number(a)>i)||void 0!==o&&Number(n)===e&&Number(a)===i&&Number(s)>=o})(t,2022,11))||null===(e=customElements.get("hui-button-card"))||void 0===e||e.getConfigElement(),customElements.get("ha-entity-picker")||null===(i=customElements.get("hui-entities-card"))||void 0===i||i.getConfigElement()},Nb=["theme","autoStartStopEnableEntity","powerEntity","section_all_themes","section_classic_only","disable_name","disable_window","disable_autoStartStop","disable_overpowering","disable_heat","disable_cool","disable_heat_cool","disable_auto","disable_dry","disable_fan_only","disable_off","disable_sleep","disable_menu","disable_safety_warning","disable_buttons","set_current_as_main","disable_power_infos","disable_auto_fan_infos","disable_target_icon","allow_lock_toggle","disable_presets","disable_timed_preset","use_manual_duration_input","allow_preset_modification","show_status_text","show_background_gradient_on_active","show_regulation_chart"],Fb=xn(((t,e)=>[{name:"entity",selector:{entity:{domain:["climate"]}}},{name:"name",selector:{text:{}}},{name:"theme",selector:{select:{options:t}}},{type:"grid",name:"",schema:[{name:"autoStartStopEnableEntity",selector:{entity:{domain:["switch"]}}},{name:"powerEntity",selector:{entity:{domain:["sensor","input_number"]}}}]},{type:"expandable",name:"section_all_themes",icon:"mdi:cog",schema:[{name:"disable_name",selector:{boolean:{}}},{name:"disable_safety_warning",selector:{boolean:{}}},{name:"set_current_as_main",selector:{boolean:{}}},{name:"allow_lock_toggle",selector:{boolean:{}}},{name:"disable_timed_preset",selector:{boolean:{}}},{name:"use_manual_duration_input",selector:{boolean:{}}},{name:"allow_preset_modification",selector:{boolean:{}}},{name:"show_regulation_chart",selector:{boolean:{}}}]},...e?[]:[{type:"expandable",name:"section_classic_only",icon:"mdi:palette-outline",schema:[{name:"disable_window",selector:{boolean:{}}},{name:"disable_autoStartStop",selector:{boolean:{}}},{name:"disable_overpowering",selector:{boolean:{}}},{name:"disable_heat",selector:{boolean:{}}},{name:"disable_cool",selector:{boolean:{}}},{name:"disable_heat_cool",selector:{boolean:{}}},{name:"disable_auto",selector:{boolean:{}}},{name:"disable_dry",selector:{boolean:{}}},{name:"disable_fan_only",selector:{boolean:{}}},{name:"disable_off",selector:{boolean:{}}},{name:"disable_sleep",selector:{boolean:{}}},{name:"disable_menu",selector:{boolean:{}}},{name:"disable_buttons",selector:{boolean:{}}},{name:"disable_power_infos",selector:{boolean:{}}},{name:"disable_auto_fan_infos",selector:{boolean:{}}},{name:"disable_target_icon",selector:{boolean:{}}},{name:"disable_presets",selector:{boolean:{}}},{name:"show_status_text",selector:{boolean:{}}},{name:"show_background_gradient_on_active",selector:{boolean:{}}}]}]]));let Wb=Hb=class extends rt{constructor(){super(...arguments),this._computeLabel=t=>{const e=fn(this.hass);return Rb.includes(t.name)?e(`editor.card.generic.${t.name}`):Nb.includes(t.name)?e(`editor.card.climate.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),Ib(this.hass.connection.haVersion)}setConfig(t){On(t,Vb),this._config=t}render(){var t;if(!this.hass||!this._config)return B``;const e=fn(this.hass),i=[{value:"classic",label:e("editor.card.climate.theme_classic")||"Classic"},{value:"vtherm",label:e("editor.card.climate.theme_vtherm")||"VTherm"},{value:"uncolored",label:e("editor.card.climate.theme_uncolored")||"Uncolored"},{value:"gunmalmg",label:e("editor.card.climate.theme_gunmalmg")||"Gunmalmg"}],o="gunmalmg"===(null===(t=this._config)||void 0===t?void 0:t.theme),n=Fb(i,o),a=Object.assign({},this._config),s={};for(const t of Hb.ALL_THEMES_KEYS)void 0!==this._config[t]&&(s[t]=this._config[t]),delete a[t];if(a.section_all_themes=s,!o){const t={};for(const e of Hb.CLASSIC_ONLY_KEYS)void 0!==this._config[e]&&(t[e]=this._config[e]),delete a[e];a.section_classic_only=t}return B` + + `}_valueChanged(t){const e=t.detail.value,{section_all_themes:i,section_classic_only:o}=e,n=function(t,e){var i={};for(var o in t)Object.prototype.hasOwnProperty.call(t,o)&&e.indexOf(o)<0&&(i[o]=t[o]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols){var n=0;for(o=Object.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 A`SK^TuhX? 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
YPVXz8^XMejnl9vWL(u1APq$x(ZxZ( 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 w2