From 435aaf48ec5c077c775399d20ca2b360f770c2da Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 7 Feb 2024 17:23:15 +0100 Subject: [PATCH 001/213] Improved the dynamic minimum setpoint - Based on return temperature --- custom_components/sat/climate.py | 18 +-- custom_components/sat/config_flow.py | 5 + custom_components/sat/const.py | 2 + custom_components/sat/coordinator.py | 4 + custom_components/sat/minimum_setpoint.py | 121 +++++---------------- custom_components/sat/mqtt/__init__.py | 8 ++ custom_components/sat/serial/__init__.py | 7 ++ custom_components/sat/translations/en.json | 9 +- custom_components/sat/util.py | 12 ++ 9 files changed, 85 insertions(+), 101 deletions(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 855452d6..3e9c1f05 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -39,12 +39,11 @@ from .const import * from .coordinator import SatDataUpdateCoordinator, DeviceState from .entity import SatEntity -from .minimum_setpoint import MinimumSetpoint from .pwm import PWMState from .relative_modulation import RelativeModulation, RelativeModulationState from .summer_simmer import SummerSimmer from .util import create_pid_controller, create_heating_curve_controller, create_pwm_controller, convert_time_str_to_seconds, \ - calculate_derivative_per_hour + calculate_derivative_per_hour, create_minimum_setpoint_controller ATTR_ROOMS = "rooms" ATTR_WARMING_UP = "warming_up_data" @@ -175,7 +174,7 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn self.pwm = create_pwm_controller(self.heating_curve, config_entry.data, config_options) # Create the Minimum Setpoint controller - self._minimum_setpoint = MinimumSetpoint(coordinator) + self._minimum_setpoint = create_minimum_setpoint_controller(config_entry.data, config_options) # Create Relative Modulation controller self._relative_modulation = RelativeModulation(coordinator, self._heating_system) @@ -550,7 +549,7 @@ def relative_modulation_state(self) -> RelativeModulationState: return self._relative_modulation.state @property - def warming_up(self): + def warming_up(self) -> bool: """Return True if we are warming up, False otherwise.""" return self._warming_up_data is not None and self._warming_up_data.elapsed < HEATER_STARTUP_TIMEFRAME @@ -563,7 +562,7 @@ def minimum_setpoint(self) -> float: @property def adjusted_minimum_setpoint(self) -> float: - return self._minimum_setpoint.current([self.error] + self.climate_errors) + return self._minimum_setpoint.current() def _calculate_control_setpoint(self) -> float: """Calculate the control setpoint based on the heating curve and PID output.""" @@ -876,8 +875,13 @@ async def async_control_heating_loop(self, _time=None) -> None: # Control the integral (if exceeded the time limit) self.pid.update_integral(self.max_error, self.heating_curve.value) - # Calculate the minimum setpoint - self._minimum_setpoint.calculate(self._coordinator.setpoint, [self.error] + self.climate_errors) + if not self._coordinator.hot_water_active and self._coordinator.flame_active: + # Calculate the base return temperature + if self.warming_up and self._coordinator.setpoint >= self._coordinator.maximum_setpoint: + self._minimum_setpoint.warming_up(self._coordinator.return_temperature) + + # Calculate the dynamic minimum setpoint + self._minimum_setpoint.calculate(self._coordinator.return_temperature) # If the setpoint is high and the HVAC is not off, turn on the heater if self._setpoint > MINIMUM_SETPOINT and self.hvac_mode != HVACMode.OFF: diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index ff89bd80..931508a1 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -450,6 +450,11 @@ async def async_step_general(self, _user_input=None) -> FlowResult: schema[vol.Required(CONF_INTEGRAL, default=options[CONF_INTEGRAL])] = str schema[vol.Required(CONF_DERIVATIVE, default=options[CONF_DERIVATIVE])] = str + if options[CONF_DYNAMIC_MINIMUM_SETPOINT]: + schema[vol.Required(CONF_MINIMUM_SETPOINT_ADJUSTMENT_FACTOR, default=options[CONF_MINIMUM_SETPOINT_ADJUSTMENT_FACTOR])] = selector.NumberSelector( + selector.NumberSelectorConfig(min=0.1, max=0.5, step=0.1) + ) + if not options[CONF_AUTOMATIC_DUTY_CYCLE]: schema[vol.Required(CONF_DUTY_CYCLE, default=options[CONF_DUTY_CYCLE])] = selector.TimeSelector() diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index 11748d0d..914cbb87 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -60,6 +60,7 @@ CONF_OUTSIDE_SENSOR_ENTITY_ID = "outside_sensor_entity_id" CONF_HUMIDITY_SENSOR_ENTITY_ID = "humidity_sensor_entity_id" CONF_DYNAMIC_MINIMUM_SETPOINT = "dynamic_minimum_setpoint" +CONF_MINIMUM_SETPOINT_ADJUSTMENT_FACTOR = "minimum_setpoint_adjustment_factor" CONF_HEATING_SYSTEM = "heating_system" CONF_HEATING_CURVE_VERSION = "heating_curve_version" @@ -91,6 +92,7 @@ CONF_DERIVATIVE_TIME_WEIGHT: 6.0, CONF_OVERSHOOT_PROTECTION: False, CONF_DYNAMIC_MINIMUM_SETPOINT: False, + CONF_MINIMUM_SETPOINT_ADJUSTMENT_FACTOR: 0.2, CONF_SECONDARY_CLIMATES: [], CONF_MAIN_CLIMATES: [], diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index 46a9ed56..a1eeb1d4 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -100,6 +100,10 @@ def hot_water_setpoint(self) -> float | None: def boiler_temperature(self) -> float | None: return None + @property + def return_temperature(self) -> float | None: + return None + @property def filtered_boiler_temperature(self) -> float | None: # Not able to use if we do not have at least two values diff --git a/custom_components/sat/minimum_setpoint.py b/custom_components/sat/minimum_setpoint.py index 388989a0..2827e89e 100644 --- a/custom_components/sat/minimum_setpoint.py +++ b/custom_components/sat/minimum_setpoint.py @@ -1,118 +1,57 @@ -import hashlib import logging -import time -from typing import List from homeassistant.core import HomeAssistant from homeassistant.helpers.storage import Store -from custom_components.sat.coordinator import SatDataUpdateCoordinator - _LOGGER = logging.getLogger(__name__) -def _is_valid(data): - if not isinstance(data, dict): - return False - - if not 'value' in data or not isinstance(data['value'], float): - return False - - if not 'timestamp' in data or not isinstance(data['timestamp'], int): - return False - - return True - - class MinimumSetpoint: _STORAGE_VERSION = 1 _STORAGE_KEY = "minimum_setpoint" - def __init__(self, coordinator: SatDataUpdateCoordinator): - self._alpha = 0.2 + def __init__(self, adjustment_factor: float, configured_minimum_setpoint: float): self._store = None - self._adjusted_setpoints = {} - self._coordinator = coordinator - self._previous_adjusted_setpoint = None + self.base_return_temperature = None + self.current_minimum_setpoint = None + self.recent_return_temperatures = [] - @staticmethod - def _get_cache_key(errors: List[float]) -> str: - errors_str = ','.join(map(str, errors)) - cache_hash = hashlib.sha256(errors_str.encode('utf-8')) - return cache_hash.hexdigest() + self.adjustment_factor = adjustment_factor + self.configured_minimum_setpoint = configured_minimum_setpoint async def async_initialize(self, hass: HomeAssistant) -> None: self._store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY) - if (adjusted_setpoints := await self._store.async_load()) is None: - adjusted_setpoints = {} - - self._adjusted_setpoints = adjusted_setpoints - - def calculate(self, setpoint: float, errors: List[float], adjustment_percentage=10): - # Check for a valid setpoint - if setpoint is None: - return - - # Calculate a cache key for adjusted setpoints - hash_key = self._get_cache_key(errors) + data = await self._store.async_load() + if data and "base_return_temperature" in data: + self.base_return_temperature = data["base_return_temperature"] + _LOGGER.debug("Loaded base return temperature from storage.") - # Extract relevant values from the coordinator for clarity - boiler_temperature = self._coordinator.boiler_temperature - target_setpoint_temperature = self._coordinator.setpoint - is_flame_active = self._coordinator.flame_active + def warming_up(self, return_temperature: float) -> None: + self.recent_return_temperatures.append(return_temperature) + self.recent_return_temperatures = self.recent_return_temperatures[-100:] - # Check for None values - if boiler_temperature is None or target_setpoint_temperature is None: - return + # Directly calculate the 90th percentile of the recent return temperatures here + if self.recent_return_temperatures: + sorted_temperatures = sorted(self.recent_return_temperatures) + index = int(len(sorted_temperatures) * 0.9) - 1 + self.base_return_temperature = sorted_temperatures[max(index, 0)] - # Check for flame activity and if we are stable - if not is_flame_active or abs(target_setpoint_temperature - boiler_temperature) <= 1: - return + if self._store and self.base_return_temperature is not None: + self._store.async_delay_save(self._data_to_save) + _LOGGER.debug("Stored base return temperature changes.") - # Check if we are above configured minimum setpoint, does not make sense if we are below it - if boiler_temperature <= self._coordinator.minimum_setpoint: + def calculate(self, return_temperature: float) -> None: + if self.base_return_temperature is None: return - # Dynamically adjust the minimum setpoint - adjustment_value = (adjustment_percentage / 100) * (target_setpoint_temperature - boiler_temperature) - raw_adjusted_setpoint = max(boiler_temperature, target_setpoint_temperature - adjustment_value) - - adjusted_setpoint = raw_adjusted_setpoint - if hash_key in self._adjusted_setpoints: - # Determine some defaults - previous_adjusted_setpoint = self._previous_adjusted_setpoint - if setpoint in self._adjusted_setpoints[hash_key]: - previous_adjusted_setpoint = self._adjusted_setpoints[hash_key][setpoint]['value'] - - # Use the moving average to adjust the calculated setpoint - if previous_adjusted_setpoint is not None: - adjusted_setpoint = self._alpha * raw_adjusted_setpoint + (1 - self._alpha) * previous_adjusted_setpoint - else: - self._adjusted_setpoints[hash_key] = {} - - # Keep track of the adjusted setpoint and update the timestamp - self._adjusted_setpoints[hash_key][setpoint] = { - 'errors': errors, - 'timestamp': int(time.time()), - 'value': round(adjusted_setpoint, 1) - } - - # Store previous value, so we have a moving value - self._previous_adjusted_setpoint = round(adjusted_setpoint, 1) - - # Store the change calibration - if self._store is not None: - self._store.async_delay_save(lambda: self._adjusted_setpoints) - - def current(self, errors: List[float]) -> float: - cache_key = self._get_cache_key(errors) + adjustment = (self.base_return_temperature - return_temperature) * self.adjustment_factor + self.current_minimum_setpoint = self.configured_minimum_setpoint + adjustment - if (data := self._adjusted_setpoints.get(cache_key)) is None: - return self._coordinator.minimum_setpoint + 2 + _LOGGER.debug(f"Calculated new minimum setpoint: {self.current_minimum_setpoint}") - return min(data.values(), key=lambda x: x['value'])['value'] + def current(self) -> float: + return self.current_minimum_setpoint if self.current_minimum_setpoint is not None else self.configured_minimum_setpoint - @property - def cache(self) -> dict[str, float]: - return self._adjusted_setpoints + def _data_to_save(self) -> dict: + return {"base_return_temperature": self.base_return_temperature} diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index b6d0ff55..fea53373 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -20,6 +20,7 @@ DATA_CONTROL_SETPOINT = "TSet" DATA_REL_MOD_LEVEL = "RelModLevel" DATA_BOILER_TEMPERATURE = "Tboiler" +DATA_RETURN_TEMPERATURE = "Tret " DATA_DHW_ENABLE = "domestichotwater" DATA_CENTRAL_HEATING = "centralheating" DATA_BOILER_CAPACITY = "MaxCapacityMinModLevel_hb_u8" @@ -112,6 +113,13 @@ def boiler_temperature(self) -> float | None: return super().boiler_temperature + @property + def return_temperature(self) -> float | None: + if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_RETURN_TEMPERATURE)) is not None: + return float(value) + + return super().return_temperature + @property def relative_modulation_value(self) -> float | None: if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_REL_MOD_LEVEL)) is not None: diff --git a/custom_components/sat/serial/__init__.py b/custom_components/sat/serial/__init__.py index 772eaae4..2f093ab9 100644 --- a/custom_components/sat/serial/__init__.py +++ b/custom_components/sat/serial/__init__.py @@ -86,6 +86,13 @@ def boiler_temperature(self) -> float | None: return super().boiler_temperature + @property + def return_temperature(self) -> float | None: + if (value := self.get(DATA_RETURN_WATER_TEMP)) is not None: + return float(value) + + return super().return_temperature + @property def minimum_hot_water_setpoint(self) -> float: if (setpoint := self.get(DATA_SLAVE_DHW_MIN_SETP)) is not None: diff --git a/custom_components/sat/translations/en.json b/custom_components/sat/translations/en.json index 311a0857..cfee4b0b 100644 --- a/custom_components/sat/translations/en.json +++ b/custom_components/sat/translations/en.json @@ -150,7 +150,8 @@ "heating_curve_version": "Heating Curve Version", "heating_curve_coefficient": "Heating Curve Coefficient", "duty_cycle": "Maximum Duty Cycle for Pulse Width Modulation", - "sync_with_thermostat": "Synchronize setpoint with thermostat" + "sync_with_thermostat": "Synchronize setpoint with thermostat", + "minimum_setpoint_adjustment_factor": "Adjustment Factor for Return Temperature" }, "data_description": { "integral": "The integral term (kI) in the PID controller, responsible for reducing steady-state error.", @@ -162,7 +163,8 @@ "heating_curve_coefficient": "The coefficient used to adjust the heating curve.", "duty_cycle": "The maximum duty cycle for Pulse Width Modulation (PWM), controlling the boiler's on-off cycles.", "sync_with_thermostat": "Synchronize setpoint with thermostat to ensure coordinated temperature control.", - "derivative_time_weight": "A parameter to adjust the influence of the derivative term over time, particularly useful for reducing undershoot during the warm-up phase when the heating curve coefficient is correctly set." + "derivative_time_weight": "A parameter to adjust the influence of the derivative term over time, particularly useful for reducing undershoot during the warm-up phase when the heating curve coefficient is correctly set.", + "minimum_setpoint_adjustment_factor": "This factor adjusts the heating setpoint based on the boiler's return temperature, affecting heating responsiveness and efficiency. A higher value increases sensitivity to temperature changes, enhancing control over comfort and energy use. Recommended starting range is 0.1 to 0.5. Adjust to suit your system and comfort preferences." } }, "presets": { @@ -214,7 +216,8 @@ "climate_valve_offset": "Offset to adjust the opening degree of the climate valve.", "target_temperature_step": "Adjust the target temperature step for fine-tuning comfort levels.", "sample_time": "The minimum time interval between updates to the PID controller.", - "maximum_relative_modulation": "Representing the highest modulation level for an efficient heating system." + "maximum_relative_modulation": "Representing the highest modulation level for an efficient heating system.", + "dynamic_minimum_setpoint": "Activates the dynamic setpoint adjustment based on the boiler's return temperature, which also helps identify if any valves are closed." } } } diff --git a/custom_components/sat/util.py b/custom_components/sat/util.py index 3a9b6de3..b5c5e949 100644 --- a/custom_components/sat/util.py +++ b/custom_components/sat/util.py @@ -1,12 +1,17 @@ from re import sub +from typing import TYPE_CHECKING from homeassistant.util import dt from .const import * from .heating_curve import HeatingCurve +from .minimum_setpoint import MinimumSetpoint from .pid import PID from .pwm import PWM +if TYPE_CHECKING: + pass + def convert_time_str_to_seconds(time_str: str) -> float: """Convert a time string in the format 'HH:MM:SS' to seconds. @@ -85,6 +90,13 @@ def create_pwm_controller(heating_curve: HeatingCurve, config_data, config_optio return PWM(heating_curve=heating_curve, max_cycle_time=max_cycle_time, automatic_duty_cycle=automatic_duty_cycle, force=force) +def create_minimum_setpoint_controller(config_data, config_options) -> MinimumSetpoint: + minimum_setpoint = config_data.get(CONF_MINIMUM_SETPOINT) + adjustment_factor = config_options.get(CONF_MINIMUM_SETPOINT_ADJUSTMENT_FACTOR) + + return MinimumSetpoint(configured_minimum_setpoint=minimum_setpoint, adjustment_factor=adjustment_factor) + + def snake_case(s): return '_'.join( sub('([A-Z][a-z]+)', r' \1', From 74095cd07fcd889ddeeae763915b6fcc2661d905 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 7 Feb 2024 17:24:15 +0100 Subject: [PATCH 002/213] Typo? --- custom_components/sat/mqtt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index fea53373..73b812b0 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -20,7 +20,7 @@ DATA_CONTROL_SETPOINT = "TSet" DATA_REL_MOD_LEVEL = "RelModLevel" DATA_BOILER_TEMPERATURE = "Tboiler" -DATA_RETURN_TEMPERATURE = "Tret " +DATA_RETURN_TEMPERATURE = "Tret" DATA_DHW_ENABLE = "domestichotwater" DATA_CENTRAL_HEATING = "centralheating" DATA_BOILER_CAPACITY = "MaxCapacityMinModLevel_hb_u8" From fcbdc06d1367cc592e51c10e4ef48b83749d277d Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 7 Feb 2024 17:24:42 +0100 Subject: [PATCH 003/213] Cleanup --- custom_components/sat/util.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/custom_components/sat/util.py b/custom_components/sat/util.py index b5c5e949..52c40b97 100644 --- a/custom_components/sat/util.py +++ b/custom_components/sat/util.py @@ -1,5 +1,4 @@ from re import sub -from typing import TYPE_CHECKING from homeassistant.util import dt @@ -9,9 +8,6 @@ from .pid import PID from .pwm import PWM -if TYPE_CHECKING: - pass - def convert_time_str_to_seconds(time_str: str) -> float: """Convert a time string in the format 'HH:MM:SS' to seconds. From 10670bf69c10b66c8e4dd82781e6a94eec49290e Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 7 Feb 2024 21:34:04 +0100 Subject: [PATCH 004/213] Reverse logic --- custom_components/sat/minimum_setpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/minimum_setpoint.py b/custom_components/sat/minimum_setpoint.py index 2827e89e..2ed28269 100644 --- a/custom_components/sat/minimum_setpoint.py +++ b/custom_components/sat/minimum_setpoint.py @@ -45,7 +45,7 @@ def calculate(self, return_temperature: float) -> None: if self.base_return_temperature is None: return - adjustment = (self.base_return_temperature - return_temperature) * self.adjustment_factor + adjustment = (return_temperature - self.base_return_temperature) * self.adjustment_factor self.current_minimum_setpoint = self.configured_minimum_setpoint + adjustment _LOGGER.debug(f"Calculated new minimum setpoint: {self.current_minimum_setpoint}") From 37cdb038167e9249ff54575b1ac47b21ec81b3cf Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 11 Feb 2024 15:29:35 +0100 Subject: [PATCH 005/213] Add support for Precision Curve, Improved PID-Controller, some other improvements and updated the translations --- custom_components/sat/__init__.py | 3 + custom_components/sat/config_flow.py | 12 +- custom_components/sat/const.py | 6 +- custom_components/sat/heating_curve.py | 12 +- custom_components/sat/pid.py | 36 ++- custom_components/sat/translations/de.json | 316 +++++++++++---------- custom_components/sat/translations/en.json | 315 ++++++++++---------- custom_components/sat/translations/es.json | 314 ++++++++++---------- custom_components/sat/translations/fr.json | 314 ++++++++++---------- custom_components/sat/translations/it.json | 314 ++++++++++---------- custom_components/sat/translations/nl.json | 314 ++++++++++---------- custom_components/sat/util.py | 2 + 12 files changed, 1010 insertions(+), 948 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index f491a5f2..37bdf4b1 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -143,6 +143,9 @@ async def async_migrate_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool if _entry.version < 6: new_options[CONF_HEATING_CURVE_VERSION] = 1 + if _entry.version < 7: + new_options[CONF_PID_CONTROLLER_VERSION] = 1 + _entry.version = SatFlowHandler.VERSION _hass.config_entries.async_update_entry(_entry, data=new_data, options=new_options) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 931508a1..8ec98310 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -35,7 +35,7 @@ class SatFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for SAT.""" - VERSION = 6 + VERSION = 7 MINOR_VERSION = 0 calibration = None @@ -426,7 +426,15 @@ async def async_step_general(self, _user_input=None) -> FlowResult: schema[vol.Required(CONF_HEATING_CURVE_VERSION, default=str(options[CONF_HEATING_CURVE_VERSION]))] = selector.SelectSelector( selector.SelectSelectorConfig(mode=SelectSelectorMode.DROPDOWN, options=[ selector.SelectOptionDict(value="1", label="Classic Curve"), - selector.SelectOptionDict(value="2", label="Quantum Curve") + selector.SelectOptionDict(value="2", label="Quantum Curve"), + selector.SelectOptionDict(value="3", label="Precision Curve"), + ]) + ) + + schema[vol.Required(CONF_PID_CONTROLLER_VERSION, default=str(options[CONF_PID_CONTROLLER_VERSION]))] = selector.SelectSelector( + selector.SelectSelectorConfig(mode=SelectSelectorMode.DROPDOWN, options=[ + selector.SelectOptionDict(value="1", label="Classic Controller"), + selector.SelectOptionDict(value="2", label="Improved Controller") ]) ) diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index 914cbb87..f5a69675 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -66,6 +66,8 @@ CONF_HEATING_CURVE_VERSION = "heating_curve_version" CONF_HEATING_CURVE_COEFFICIENT = "heating_curve_coefficient" +CONF_PID_CONTROLLER_VERSION = "pid_controller_version" + CONF_MINIMUM_CONSUMPTION = "minimum_consumption" CONF_MAXIMUM_CONSUMPTION = "maximum_consumption" @@ -128,9 +130,11 @@ CONF_SLEEP_TEMPERATURE: 15, CONF_COMFORT_TEMPERATURE: 20, - CONF_HEATING_CURVE_VERSION: 2, + CONF_HEATING_CURVE_VERSION: 3, CONF_HEATING_CURVE_COEFFICIENT: 1.0, CONF_HEATING_SYSTEM: HEATING_SYSTEM_RADIATORS, + + CONF_PID_CONTROLLER_VERSION: 2, } # Storage diff --git a/custom_components/sat/heating_curve.py b/custom_components/sat/heating_curve.py index 77a7c08f..ba5e5884 100644 --- a/custom_components/sat/heating_curve.py +++ b/custom_components/sat/heating_curve.py @@ -8,7 +8,7 @@ class HeatingCurve: - def __init__(self, heating_system: str, coefficient: float, version: int = 2): + def __init__(self, heating_system: str, coefficient: float, version: int = 3): """ :param heating_system: The type of heating system, either "underfloor" or "radiator" :param coefficient: The coefficient used to adjust the heating curve @@ -72,10 +72,16 @@ def restore_autotune(self, coefficient: float, derivative: float): def _get_heating_curve_value(self, target_temperature: float, outside_temperature: float) -> float: """Calculate the heating curve value based on the current outside temperature""" - if self._version <= 1: + if self._version == 1: return target_temperature - (0.01 * outside_temperature ** 2) - (0.8 * outside_temperature) - return 2.72 * (target_temperature - 20) + 0.03 * (outside_temperature - 20) ** 2 - 1.2 * (outside_temperature - 20) + if self._version == 2: + return 2.72 * (target_temperature - 20) + 0.03 * (outside_temperature - 20) ** 2 - 1.2 * (outside_temperature - 20) + + if self._version == 3: + return 4 * (target_temperature - 20) + 0.03 * (outside_temperature - 20) ** 2 - 0.4 * (outside_temperature - 20) + + raise Exception("Invalid version") @property def base_offset(self) -> float: diff --git a/custom_components/sat/pid.py b/custom_components/sat/pid.py index 05664bc4..4e59f1b4 100644 --- a/custom_components/sat/pid.py +++ b/custom_components/sat/pid.py @@ -24,7 +24,8 @@ def __init__(self, deadband: float = DEADBAND, automatic_gains: bool = False, integral_time_limit: float = 300, - sample_time_limit: Optional[float] = 10): + sample_time_limit: Optional[float] = 10, + version: int = 2): """ Initialize the PID controller. @@ -38,10 +39,12 @@ def __init__(self, :param deadband: The deadband of the PID controller. The range of error values where the controller will not make adjustments. :param integral_time_limit: The minimum time interval between integral updates to the PID controller, in seconds. :param sample_time_limit: The minimum time interval between updates to the PID controller, in seconds. + :param version: The version of math calculation for the controller. """ self._kp = kp self._ki = ki self._kd = kd + self._version = version self._deadband = deadband self._history_size = max_history self._heating_system = heating_system @@ -178,6 +181,10 @@ def update_derivative(self, error: float, alpha1: float = 0.8, alpha2: float = 0 if len(self._errors) < 2: return + # If derivative is disabled, we freeze it + if not self.derivative_enabled: + return + # Calculate the derivative using the errors and times in the error history. num_of_errors = len(self._errors) time_elapsed = self._times[num_of_errors - 1] - self._times[0] @@ -242,6 +249,15 @@ def restore(self, state: State) -> None: if last_heating_curve := state.attributes.get("heating_curve"): self._last_heating_curve_value = last_heating_curve + def _get_aggression_value(self): + if self._version == 1: + return 73 if self._heating_system == HEATING_SYSTEM_UNDERFLOOR else 99 + + if self._version == 2: + return 73 if self._heating_system == HEATING_SYSTEM_UNDERFLOOR else 81.5 + + raise Exception("Invalid version") + @property def last_error(self) -> float: """Return the last error value used by the PID controller.""" @@ -276,22 +292,24 @@ def ki(self) -> float | None: if self._last_heating_curve_value is None: return 0 - return round(self._last_heating_curve_value / 73900, 6) + if self._version == 1: + return round(self._last_heating_curve_value / 73900, 6) + + if self._version == 2: + return round(self._automatic_gains_value * (self._last_heating_curve_value / 36000), 6) + + raise Exception("Invalid version") return float(self._ki) @property def kd(self) -> float | None: """Return the value of kd based on the current configuration.""" - if not self.derivative_enabled: - return 0 - if self._automatic_gains: if self._last_heating_curve_value is None: return 0 - aggression_value = 73 if self._heating_system == HEATING_SYSTEM_UNDERFLOOR else 99 - return round(self._automatic_gains_value * aggression_value * self._derivative_time_weight * self._last_heating_curve_value, 6) + return round(self._automatic_gains_value * self._get_aggression_value() * self._derivative_time_weight * self._last_heating_curve_value, 6) return float(self._kd) @@ -333,12 +351,12 @@ def output(self) -> float: @property def integral_enabled(self) -> bool: """Return whether the updates of the integral are enabled.""" - return abs(self._last_error) <= self._deadband + return abs(self.previous_error) <= self._deadband @property def derivative_enabled(self) -> bool: """Return whether the updates of the derivative are enabled.""" - return abs(self.last_error) > self._deadband or abs(self.previous_error) > self._deadband + return abs(self.previous_error) > self._deadband @property def num_errors(self) -> int: diff --git a/custom_components/sat/translations/de.json b/custom_components/sat/translations/de.json index f9643559..e62ae8ef 100644 --- a/custom_components/sat/translations/de.json +++ b/custom_components/sat/translations/de.json @@ -1,222 +1,226 @@ { "config": { + "abort": { + "already_configured": "Gateway ist bereits konfiguriert." + }, + "error": { + "connection": "Verbindung zum Gateway konnte nicht hergestellt werden.", + "mqtt_component": "Die MQTT-Komponente ist nicht verfügbar.", + "unable_to_calibrate": "Der Kalibrierungsprozess ist auf ein Problem gestoßen und konnte nicht erfolgreich abgeschlossen werden. Bitte stellen Sie sicher, dass Ihr Heizsystem ordnungsgemäß funktioniert und dass alle erforderlichen Sensoren angeschlossen und funktionsfähig sind.\n\nWenn Sie weiterhin Probleme mit der Kalibrierung haben, ziehen Sie in Erwägung, uns für weitere Unterstützung zu kontaktieren. Wir entschuldigen uns für etwaige Unannehmlichkeiten." + }, + "progress": { + "calibration": "Kalibrierung und Ermittlung des Überschwingungsschutzwertes...\n\nBitte warten Sie, während wir Ihr Heizsystem optimieren. Dieser Prozess kann etwa 20 Minuten dauern." + }, "step": { - "user": { - "title": "Smart Autotune Thermostat (SAT)", - "description": "SAT ist ein intelligentes Thermostat, das in der Lage ist, sich selbst automatisch zu justieren, um die Temperaturregelung zu optimieren. Wählen Sie den passenden Modus, der zu Ihrem Heizsystem passt.", - "menu_options": { - "mosquitto": "OpenTherm Gateway ( MQTT )", - "serial": "OpenTherm Gateway ( SERIELL )", - "switch": "PID-Thermostat mit PWM ( EIN/AUS )", - "simulator": "Simuliertes Gateway ( FORTGESCHRITTEN )" - } - }, - "mosquitto": { - "title": "OpenTherm Gateway ( MQTT )", - "description": "Bitte geben Sie die folgenden Details an, um das OpenTherm Gateway einzurichten. Geben Sie im Feld Name einen Namen für das Gateway ein, der Ihnen hilft, es in Ihrem System zu identifizieren.\n\nGeben Sie die Klimaentität an, die für das OpenTherm Gateway verwendet wird. Diese Entität wird vom OpenTherm Gateway bereitgestellt und repräsentiert Ihr Heizsystem.\n\nGeben Sie außerdem das Top-Thema an, das für das Veröffentlichen und Abonnieren von MQTT-Nachrichten im Zusammenhang mit dem OpenTherm Gateway verwendet wird.\n\nDiese Einstellungen sind wesentlich, um die Kommunikation und Integration mit Ihrem OpenTherm Gateway über MQTT herzustellen. Sie ermöglichen einen nahtlosen Datenaustausch und die Steuerung Ihres Heizsystems. Stellen Sie sicher, dass die angegebenen Details korrekt sind, um eine ordnungsgemäße Funktionalität zu gewährleisten.", - "data": { - "name": "Name", - "device": "Gerät", - "mqtt_topic": "Top-Thema" - } - }, - "serial": { - "title": "OpenTherm Gateway ( SERIELL )", - "description": "Um eine Verbindung mit dem OpenTherm Gateway über eine Socket-Verbindung herzustellen, geben Sie bitte die folgenden Details an. Geben Sie im Feld Name einen Namen für das Gateway ein, der Ihnen hilft, es in Ihrem System zu identifizieren.\n\nGeben Sie die Netzwerkadresse des OpenTherm Gateways im Feld Gerät an. Dies könnte im Format \"socket://otgw.local:25238\" sein, wobei \"otgw.local\" der Hostname oder die IP-Adresse des Gateways ist und \"25238\" die Portnummer.\n\nDiese Einstellungen sind wesentlich, um die Kommunikation und Integration mit Ihrem OpenTherm Gateway über die Socket-Verbindung herzustellen. Stellen Sie sicher, dass die angegebenen Details korrekt sind, um eine ordnungsgemäße Funktionalität zu gewährleisten.", - "data": { - "name": "Name", - "device": "URL" - } - }, - "switch": { - "title": "PID-Thermostat mit PWM ( EIN/AUS )", - "description": "Bitte füllen Sie die folgenden Details aus, um den Schalter einzurichten. Geben Sie einen Namen für den Schalter im Feld Name ein, der Ihnen hilft, ihn in Ihrem System zu identifizieren. Wählen Sie die passende Entität für Ihren Schalter aus den angebotenen Optionen aus.\n\nGeben Sie im Feld Temperatureinstellung die gewünschte Zieltemperatur für Ihr Heizsystem an. Wenn Sie einen Warmwasserboiler verwenden, geben Sie die Boilertemperatureinstellung mit dem entsprechenden Wert an. Für elektrische Heizsysteme geben Sie den Wert 100 ein.\n\nDiese Einstellungen sind wesentlich für eine präzise Temperaturregelung und gewährleisten eine optimale Leistung Ihres Heizsystems. Die Angabe der korrekten Temperatureinstellung ermöglicht eine genaue Regelung und hilft dabei, eine komfortable und energieeffiziente Umgebung in Ihrem Zuhause zu erreichen.", - "data": { - "name": "Name", - "device": "Entität", - "minimum_setpoint": "Temperatureinstellung" - } - }, - "simulator": { - "title": "Simuliertes Gateway ( FORTGESCHRITTEN )", - "description": "Dieses Gateway ermöglicht es Ihnen, einen Boiler zu simulieren, zu Test- und Demonstrationszwecken. Bitte geben Sie die folgenden Informationen an, um den Simulator zu konfigurieren.\n\nHinweis: Das Simulator Gateway ist nur zu Test- und Demonstrationszwecken gedacht und sollte nicht in Produktionsumgebungen verwendet werden.", - "data": { - "name": "Name", - "minimum_setpoint": "Mindesteinstellung", - "maximum_setpoint": "Maximale Einstellung", - "simulated_heating": "Simulierte Heizung", - "simulated_cooling": "Simulierte Kühlung", - "simulated_warming_up": "Simuliertes Aufwärmen" - } - }, - "sensors": { - "title": "Sensoren konfigurieren", - "description": "Bitte wählen Sie die Sensoren aus, die zur Überwachung der Temperatur verwendet werden sollen.", - "data": { - "inside_sensor_entity_id": "Innensensor-Entität", - "outside_sensor_entity_id": "Außensensor-Entität", - "humidity_sensor_entity_id": "Feuchtigkeitssensor-Entität" - } - }, - "heating_system": { - "title": "Heizsystem", - "description": "Die Auswahl des richtigen Heizsystemtyps ist wichtig, damit SAT die Temperatur genau steuern und die Leistung optimieren kann. Wählen Sie die Option, die zu Ihrer Einrichtung passt, um eine ordnungsgemäße Temperaturregelung in Ihrem Zuhause zu gewährleisten.", - "data": { - "heating_system": "System" - } - }, "areas": { - "title": "Bereiche", - "description": "Einstellungen im Zusammenhang mit Klimazonen, Mehrzimmern und Temperaturregelung. Primäre Klimazonen befinden sich im selben Raum wie der Innensensor, und die Zimmer haben ihre eigenen Zieltemperaturen, getrennt vom System.", "data": { "main_climates": "Primär", "secondary_climates": "Zimmer" - } + }, + "description": "Einstellungen im Zusammenhang mit Klimazonen, Mehrzimmern und Temperaturregelung. Primäre Klimazonen befinden sich im selben Raum wie der Innensensor, und die Zimmer haben ihre eigenen Zieltemperaturen, getrennt vom System.", + "title": "Bereiche" }, "automatic_gains": { - "title": "Automatische Verstärkungen", - "description": "Diese Funktion passt die Steuerungsparameter Ihres Heizsystems dynamisch an, um die Temperaturregelung für mehr Komfort und Energieeffizienz zu optimieren. Die Aktivierung dieser Option ermöglicht es SAT, die Heizeinstellungen kontinuierlich anzupassen und auf der Grundlage der Umgebungsbedingungen fein abzustimmen. Dies hilft, eine stabile und komfortable Umgebung ohne manuelle Eingriffe aufrechtzuerhalten.\n\nHinweis: Wenn Sie sich entscheiden, automatische Verstärkungen nicht zu aktivieren, müssen Sie die PID-Werte manuell eingeben, um eine präzise Temperaturregelung zu gewährleisten. Bitte stellen Sie sicher, dass Sie genaue PID-Werte für Ihr spezifisches Heizsystem haben, um eine optimale Leistung zu erzielen.", "data": { "automatic_gains": "Automatische Verstärkungen (empfohlen)" - } + }, + "description": "Diese Funktion passt die Steuerungsparameter Ihres Heizsystems dynamisch an, um die Temperaturregelung für mehr Komfort und Energieeffizienz zu optimieren. Die Aktivierung dieser Option ermöglicht es SAT, die Heizeinstellungen kontinuierlich anzupassen und auf der Grundlage der Umgebungsbedingungen fein abzustimmen. Dies hilft, eine stabile und komfortable Umgebung ohne manuelle Eingriffe aufrechtzuerhalten.\n\nHinweis: Wenn Sie sich entscheiden, automatische Verstärkungen nicht zu aktivieren, müssen Sie die PID-Werte manuell eingeben, um eine präzise Temperaturregelung zu gewährleisten. Bitte stellen Sie sicher, dass Sie genaue PID-Werte für Ihr spezifisches Heizsystem haben, um eine optimale Leistung zu erzielen.", + "title": "Automatische Verstärkungen" }, "calibrate_system": { - "title": "System kalibrieren", "description": "Optimieren Sie Ihr Heizsystem, indem Sie die optimalen PID-Werte für Ihre Einrichtung automatisch bestimmen. Bitte beachten Sie, dass das System bei Auswahl von Automatischen Verstärkungen einen Kalibrierungsprozess durchläuft, der etwa 20 Minuten dauern kann.\n\nAutomatische Verstärkungen werden für die meisten Benutzer empfohlen, da sie den Einrichtungsprozess vereinfachen und eine optimale Leistung gewährleisten. Wenn Sie jedoch mit der PID-Steuerung vertraut sind und die Werte lieber manuell eingeben möchten, können Sie sich entscheiden, Automatische Verstärkungen zu überspringen.\n\nBitte beachten Sie, dass die Entscheidung gegen Automatische Verstärkungen ein gutes Verständnis der PID-Steuerung erfordert und möglicherweise zusätzliche manuelle Anpassungen für eine optimale Leistung notwendig sind.", "menu_options": { "calibrate": "Kalibrieren und Ihren Überschwingungsschutzwert bestimmen (ca. 20 Min.).", "overshoot_protection": "Den Überschwingungsschutzwert manuell eingeben.", "pid_controller": "PID-Werte manuell eingeben (nicht empfohlen)." - } + }, + "title": "System kalibrieren" + }, + "calibrated": { + "description": "Der Kalibrierungsprozess wurde erfolgreich abgeschlossen.\n\nHerzlichen Glückwunsch! Ihr Smart Autotune Thermostat (SAT) wurde kalibriert, um die Heizleistung Ihres Systems zu optimieren. Während des Kalibrierungsprozesses hat SAT die Heizeigenschaften sorgfältig analysiert und den angemessenen Überschwingungsschutzwert bestimmt, um eine präzise Temperaturregelung zu gewährleisten.\n\nÜberschwingungsschutzwert: {minimum_setpoint} °C\n\nDieser Wert stellt die maximale Menge an Überschwingen dar, die während des Heizprozesses erlaubt ist. SAT wird aktiv die Heizung überwachen und anpassen, um übermäßiges Überschwingen zu verhindern und eine komfortable und effiziente Heizerfahrung in Ihrem Zuhause zu gewährleisten.\n\nBitte beachten Sie, dass der Überschwingungsschutzwert je nach den spezifischen Eigenschaften Ihres Heizsystems und Umweltfaktoren variieren kann. Er wurde fein abgestimmt, um basierend auf den Kalibrierungsergebnissen eine optimale Leistung zu bieten.", + "menu_options": { + "calibrate": "Kalibrierung erneut versuchen", + "finish": "Mit der aktuellen Kalibrierung fortfahren" + }, + "title": "Kalibrierung abgeschlossen" + }, + "heating_system": { + "data": { + "heating_system": "System" + }, + "description": "Die Auswahl des richtigen Heizsystemtyps ist wichtig, damit SAT die Temperatur genau steuern und die Leistung optimieren kann. Wählen Sie die Option, die zu Ihrer Einrichtung passt, um eine ordnungsgemäße Temperaturregelung in Ihrem Zuhause zu gewährleisten.", + "title": "Heizsystem" + }, + "mosquitto": { + "data": { + "device": "Gerät", + "mqtt_topic": "Top-Thema", + "name": "Name" + }, + "description": "Bitte geben Sie die folgenden Details an, um das OpenTherm Gateway einzurichten. Geben Sie im Feld Name einen Namen für das Gateway ein, der Ihnen hilft, es in Ihrem System zu identifizieren.\n\nGeben Sie die Klimaentität an, die für das OpenTherm Gateway verwendet wird. Diese Entität wird vom OpenTherm Gateway bereitgestellt und repräsentiert Ihr Heizsystem.\n\nGeben Sie außerdem das Top-Thema an, das für das Veröffentlichen und Abonnieren von MQTT-Nachrichten im Zusammenhang mit dem OpenTherm Gateway verwendet wird.\n\nDiese Einstellungen sind wesentlich, um die Kommunikation und Integration mit Ihrem OpenTherm Gateway über MQTT herzustellen. Sie ermöglichen einen nahtlosen Datenaustausch und die Steuerung Ihres Heizsystems. Stellen Sie sicher, dass die angegebenen Details korrekt sind, um eine ordnungsgemäße Funktionalität zu gewährleisten.", + "title": "OpenTherm Gateway ( MQTT )" }, "overshoot_protection": { - "title": "Überschwingungsschutz", - "description": "Durch Angabe des Überschwingungsschutzwertes wird SAT die Steuerungsparameter entsprechend anpassen, um eine stabile und komfortable Heizumgebung zu erhalten. Diese manuelle Konfiguration ermöglicht es Ihnen, das System basierend auf Ihrer spezifischen Einrichtung fein abzustimmen.\n\nHinweis: Wenn Sie sich nicht sicher über den Überschwingungsschutzwert sind oder den Kalibrierungsprozess nicht durchgeführt haben, wird empfohlen, die Konfiguration abzubrechen und den Kalibrierungsprozess durchzuführen, damit SAT den Wert automatisch für eine optimale Leistung bestimmt.", "data": { "minimum_setpoint": "Wert" - } + }, + "description": "Durch Angabe des Überschwingungsschutzwertes wird SAT die Steuerungsparameter entsprechend anpassen, um eine stabile und komfortable Heizumgebung zu erhalten. Diese manuelle Konfiguration ermöglicht es Ihnen, das System basierend auf Ihrer spezifischen Einrichtung fein abzustimmen.\n\nHinweis: Wenn Sie sich nicht sicher über den Überschwingungsschutzwert sind oder den Kalibrierungsprozess nicht durchgeführt haben, wird empfohlen, die Konfiguration abzubrechen und den Kalibrierungsprozess durchzuführen, damit SAT den Wert automatisch für eine optimale Leistung bestimmt.", + "title": "Überschwingungsschutz" }, "pid_controller": { - "title": "PID-Regler manuell konfigurieren.", - "description": "Konfigurieren Sie die proportionalen, integralen und derivativen Verstärkungen manuell, um Ihr Heizsystem fein abzustimmen. Verwenden Sie diese Option, wenn Sie die volle Kontrolle über die PID-Reglerparameter haben möchten. Passen Sie die Verstärkungen basierend auf den spezifischen Eigenschaften und Vorlieben Ihres Heizsystems an.", "data": { - "integral": "Integral (kI)", "derivative": "Derivativ (kD)", + "integral": "Integral (kI)", "proportional": "Proportional (kP)" - } + }, + "description": "Konfigurieren Sie die proportionalen, integralen und derivativen Verstärkungen manuell, um Ihr Heizsystem fein abzustimmen. Verwenden Sie diese Option, wenn Sie die volle Kontrolle über die PID-Reglerparameter haben möchten. Passen Sie die Verstärkungen basierend auf den spezifischen Eigenschaften und Vorlieben Ihres Heizsystems an.", + "title": "PID-Regler manuell konfigurieren." }, - "calibrated": { - "title": "Kalibrierung abgeschlossen", - "description": "Der Kalibrierungsprozess wurde erfolgreich abgeschlossen.\n\nHerzlichen Glückwunsch! Ihr Smart Autotune Thermostat (SAT) wurde kalibriert, um die Heizleistung Ihres Systems zu optimieren. Während des Kalibrierungsprozesses hat SAT die Heizeigenschaften sorgfältig analysiert und den angemessenen Überschwingungsschutzwert bestimmt, um eine präzise Temperaturregelung zu gewährleisten.\n\nÜberschwingungsschutzwert: {minimum_setpoint} °C\n\nDieser Wert stellt die maximale Menge an Überschwingen dar, die während des Heizprozesses erlaubt ist. SAT wird aktiv die Heizung überwachen und anpassen, um übermäßiges Überschwingen zu verhindern und eine komfortable und effiziente Heizerfahrung in Ihrem Zuhause zu gewährleisten.\n\nBitte beachten Sie, dass der Überschwingungsschutzwert je nach den spezifischen Eigenschaften Ihres Heizsystems und Umweltfaktoren variieren kann. Er wurde fein abgestimmt, um basierend auf den Kalibrierungsergebnissen eine optimale Leistung zu bieten.", + "sensors": { + "data": { + "humidity_sensor_entity_id": "Feuchtigkeitssensor-Entität", + "inside_sensor_entity_id": "Innensensor-Entität", + "outside_sensor_entity_id": "Außensensor-Entität" + }, + "description": "Bitte wählen Sie die Sensoren aus, die zur Überwachung der Temperatur verwendet werden sollen.", + "title": "Sensoren konfigurieren" + }, + "serial": { + "data": { + "device": "URL", + "name": "Name" + }, + "description": "Um eine Verbindung mit dem OpenTherm Gateway über eine Socket-Verbindung herzustellen, geben Sie bitte die folgenden Details an. Geben Sie im Feld Name einen Namen für das Gateway ein, der Ihnen hilft, es in Ihrem System zu identifizieren.\n\nGeben Sie die Netzwerkadresse des OpenTherm Gateways im Feld Gerät an. Dies könnte im Format \"socket://otgw.local:25238\" sein, wobei \"otgw.local\" der Hostname oder die IP-Adresse des Gateways ist und \"25238\" die Portnummer.\n\nDiese Einstellungen sind wesentlich, um die Kommunikation und Integration mit Ihrem OpenTherm Gateway über die Socket-Verbindung herzustellen. Stellen Sie sicher, dass die angegebenen Details korrekt sind, um eine ordnungsgemäße Funktionalität zu gewährleisten.", + "title": "OpenTherm Gateway ( SERIELL )" + }, + "simulator": { + "data": { + "maximum_setpoint": "Maximale Einstellung", + "minimum_setpoint": "Mindesteinstellung", + "name": "Name", + "simulated_cooling": "Simulierte Kühlung", + "simulated_heating": "Simulierte Heizung", + "simulated_warming_up": "Simuliertes Aufwärmen" + }, + "description": "Dieses Gateway ermöglicht es Ihnen, einen Boiler zu simulieren, zu Test- und Demonstrationszwecken. Bitte geben Sie die folgenden Informationen an, um den Simulator zu konfigurieren.\n\nHinweis: Das Simulator Gateway ist nur zu Test- und Demonstrationszwecken gedacht und sollte nicht in Produktionsumgebungen verwendet werden.", + "title": "Simuliertes Gateway ( FORTGESCHRITTEN )" + }, + "switch": { + "data": { + "device": "Entität", + "minimum_setpoint": "Temperatureinstellung", + "name": "Name" + }, + "description": "Bitte füllen Sie die folgenden Details aus, um den Schalter einzurichten. Geben Sie einen Namen für den Schalter im Feld Name ein, der Ihnen hilft, ihn in Ihrem System zu identifizieren. Wählen Sie die passende Entität für Ihren Schalter aus den angebotenen Optionen aus.\n\nGeben Sie im Feld Temperatureinstellung die gewünschte Zieltemperatur für Ihr Heizsystem an. Wenn Sie einen Warmwasserboiler verwenden, geben Sie die Boilertemperatureinstellung mit dem entsprechenden Wert an. Für elektrische Heizsysteme geben Sie den Wert 100 ein.\n\nDiese Einstellungen sind wesentlich für eine präzise Temperaturregelung und gewährleisten eine optimale Leistung Ihres Heizsystems. Die Angabe der korrekten Temperatureinstellung ermöglicht eine genaue Regelung und hilft dabei, eine komfortable und energieeffiziente Umgebung in Ihrem Zuhause zu erreichen.", + "title": "PID-Thermostat mit PWM ( EIN/AUS )" + }, + "user": { + "description": "SAT ist ein intelligentes Thermostat, das in der Lage ist, sich selbst automatisch zu justieren, um die Temperaturregelung zu optimieren. Wählen Sie den passenden Modus, der zu Ihrem Heizsystem passt.", "menu_options": { - "calibrate": "Kalibrierung erneut versuchen", - "finish": "Mit der aktuellen Kalibrierung fortfahren" - } + "mosquitto": "OpenTherm Gateway ( MQTT )", + "serial": "OpenTherm Gateway ( SERIELL )", + "simulator": "Simuliertes Gateway ( FORTGESCHRITTEN )", + "switch": "PID-Thermostat mit PWM ( EIN/AUS )" + }, + "title": "Smart Autotune Thermostat (SAT)" } - }, - "error": { - "connection": "Verbindung zum Gateway konnte nicht hergestellt werden.", - "mqtt_component": "Die MQTT-Komponente ist nicht verfügbar.", - "unable_to_calibrate": "Der Kalibrierungsprozess ist auf ein Problem gestoßen und konnte nicht erfolgreich abgeschlossen werden. Bitte stellen Sie sicher, dass Ihr Heizsystem ordnungsgemäß funktioniert und dass alle erforderlichen Sensoren angeschlossen und funktionsfähig sind.\n\nWenn Sie weiterhin Probleme mit der Kalibrierung haben, ziehen Sie in Erwägung, uns für weitere Unterstützung zu kontaktieren. Wir entschuldigen uns für etwaige Unannehmlichkeiten." - }, - "abort": { - "already_configured": "Gateway ist bereits konfiguriert." - }, - "progress": { - "calibration": "Kalibrierung und Ermittlung des Überschwingungsschutzwertes...\n\nBitte warten Sie, während wir Ihr Heizsystem optimieren. Dieser Prozess kann etwa 20 Minuten dauern." } }, "options": { "step": { - "init": { - "menu_options": { - "general": "Allgemein", - "presets": "Voreinstellungen", - "advanced": "Erweiterte Optionen", - "system_configuration": "Systemkonfiguration" - } + "advanced": { + "data": { + "climate_valve_offset": "Klimaventil-Offset", + "dynamic_minimum_setpoint": "Dynamischer Minimaler Sollwert (Experimentell)", + "force_pulse_width_modulation": "Pulsweitenmodulation erzwingen", + "maximum_consumption": "Maximalverbrauch", + "maximum_relative_modulation": "Maximale relative Modulation", + "minimum_consumption": "Mindestverbrauch", + "sample_time": "Abtastzeit", + "simulation": "Simulation", + "target_temperature_step": "Schrittweite der Zieltemperatur", + "thermal_comfort": "Thermischer Komfort" + }, + "data_description": { + "climate_valve_offset": "Offset zur Anpassung des Öffnungsgrads des Klimaventils.", + "dynamic_minimum_setpoint": "Aktiviert die dynamische Sollwertanpassung basierend auf der Rücklauftemperatur des Kessels, was auch hilft zu identifizieren, ob Ventile geschlossen sind.", + "maximum_consumption": "Der maximale Gasverbrauch, wenn der Kessel aktiv ist.", + "maximum_relative_modulation": "Die höchste Modulationsstufe für ein effizientes Heizsystem darstellen.", + "minimum_consumption": "Der minimale Gasverbrauch, wenn der Kessel aktiv ist.", + "sample_time": "Das minimale Zeitintervall zwischen Aktualisierungen des PID-Reglers.", + "target_temperature_step": "Die Schrittweite der Zieltemperatur zur Feinabstimmung der Komfortstufen anpassen.", + "thermal_comfort": "Verwendung des Simmer-Index für die Anpassung des thermischen Komforts aktivieren." + }, + "title": "Erweitert" }, "general": { - "title": "Allgemein", - "description": "Allgemeine Einstellungen und Konfigurationen.", "data": { - "integral": "Integral (kI)", - "derivative": "Ableitung (kD)", - "proportional": "Proportional (kP)", - "maximum_setpoint": "Maximaler Sollwert", - "window_sensors": "Kontaktsensoren", "automatic_gains_value": "Automatischer Verstärkungswert", + "derivative": "Ableitung (kD)", "derivative_time_weight": "Zeitgewicht der Ableitung", - "heating_curve_version": "Version der Heizkurve", - "heating_curve_coefficient": "Koeffizient der Heizkurve", "duty_cycle": "Maximaler Tastgrad für Pulsweitenmodulation", - "sync_with_thermostat": "Sollwert mit Thermostat synchronisieren" + "heating_curve_coefficient": "Koeffizient der Heizkurve", + "heating_curve_version": "Version der Heizkurve", + "integral": "Integral (kI)", + "maximum_setpoint": "Maximaler Sollwert", + "minimum_setpoint_adjustment_factor": "Anpassungsfaktor für die Rücklauftemperatur", + "pid_controller_version": "PID-Reglerversion", + "proportional": "Proportional (kP)", + "sync_with_thermostat": "Sollwert mit Thermostat synchronisieren", + "window_sensors": "Kontaktsensoren" }, "data_description": { - "integral": "Der Integralterm (kI) im PID-Regler, verantwortlich für die Reduzierung des stationären Fehlers.", - "derivative": "Der Ableitungsterm (kD) im PID-Regler, verantwortlich für die Minderung von Überschwingen.", - "proportional": "Der proportionale Term (kP) im PID-Regler, verantwortlich für die sofortige Reaktion auf Fehler.", - "maximum_setpoint": "Die optimale Temperatur für einen effizienten Kesselbetrieb.", - "window_sensors": "Kontaktsensoren, die das System auslösen, wenn ein Fenster oder eine Tür für eine bestimmte Zeit geöffnet ist.", "automatic_gains_value": "Der Wert, der für automatische Verstärkungen im PID-Regler verwendet wird.", - "heating_curve_coefficient": "Der Koeffizient, der zur Anpassung der Heizkurve verwendet wird.", + "derivative": "Der Ableitungsterm (kD) im PID-Regler, verantwortlich für die Minderung von Überschwingen.", + "derivative_time_weight": "Ein Parameter zur Anpassung des Einflusses des Ableitungsterms über die Zeit, besonders nützlich, um Unterschwingen während der Aufwärmphase zu reduzieren, wenn der Heizkurvenkoeffizient korrekt eingestellt ist.", "duty_cycle": "Der maximale Tastgrad für Pulsweitenmodulation (PWM), der die Ein-Aus-Zyklen des Kessels steuert.", + "heating_curve_coefficient": "Der Koeffizient, der zur Anpassung der Heizkurve verwendet wird.", + "integral": "Der Integralterm (kI) im PID-Regler, verantwortlich für die Reduzierung des stationären Fehlers.", + "maximum_setpoint": "Die optimale Temperatur für einen effizienten Kesselbetrieb.", + "minimum_setpoint_adjustment_factor": "Dieser Faktor passt den Heizungssollwert basierend auf der Rücklauftemperatur des Kessels an, was die Heizungsreaktivität und -effizienz beeinflusst. Ein höherer Wert erhöht die Sensibilität gegenüber Temperaturänderungen und verbessert die Kontrolle über Komfort und Energieverbrauch. Empfohlener Startbereich liegt zwischen 0,1 und 0,5. Anpassen, um Ihrem System und Ihren Komfortpräferenzen zu entsprechen.", + "proportional": "Der proportionale Term (kP) im PID-Regler, verantwortlich für die sofortige Reaktion auf Fehler.", "sync_with_thermostat": "Sollwert mit Thermostat synchronisieren, um eine koordinierte Temperaturregelung zu gewährleisten.", - "derivative_time_weight": "Ein Parameter zur Anpassung des Einflusses des Ableitungsterms über die Zeit, besonders nützlich, um Unterschwingen während der Aufwärmphase zu reduzieren, wenn der Heizkurvenkoeffizient korrekt eingestellt ist." + "window_sensors": "Kontaktsensoren, die das System auslösen, wenn ein Fenster oder eine Tür für eine bestimmte Zeit geöffnet ist." + }, + "description": "Allgemeine Einstellungen und Konfigurationen.", + "title": "Allgemein" + }, + "init": { + "menu_options": { + "advanced": "Erweiterte Optionen", + "general": "Allgemein", + "presets": "Voreinstellungen", + "system_configuration": "Systemkonfiguration" } }, "presets": { - "title": "Voreinstellungen", - "description": "Vordefinierte Temperatureinstellungen für verschiedene Szenarien oder Aktivitäten.", "data": { + "activity_temperature": "Aktivitätstemperatur", "away_temperature": "Abwesenheitstemperatur", + "comfort_temperature": "Komforttemperatur", "home_temperature": "Heimtemperatur", "sleep_temperature": "Schlafzimmertemperatur", - "comfort_temperature": "Komforttemperatur", - "activity_temperature": "Aktivitätstemperatur", "sync_climates_with_preset": "Klimazonen mit Voreinstellung synchronisieren (schlafen/abwesend/aktiv)" - } + }, + "description": "Vordefinierte Temperatureinstellungen für verschiedene Szenarien oder Aktivitäten.", + "title": "Voreinstellungen" }, "system_configuration": { - "title": "Systemkonfiguration", - "description": "Für Feinabstimmung und Anpassung.", "data": { "automatic_duty_cycle": "Automatischer Tastgrad", "overshoot_protection": "Überschwingungsschutz (mit PWM)", - "window_minimum_open_time": "Mindestzeit für geöffnetes Fenster", - "sensor_max_value_age": "Maximales Alter des Temperatursensorwerts" + "sensor_max_value_age": "Maximales Alter des Temperatursensorwerts", + "window_minimum_open_time": "Mindestzeit für geöffnetes Fenster" }, "data_description": { "automatic_duty_cycle": "Automatischen Tastgrad für Pulsweitenmodulation (PWM) aktivieren oder deaktivieren.", "overshoot_protection": "Überschwingungsschutz mit Pulsweitenmodulation (PWM) aktivieren, um die Temperaturüberschreitung des Kessels zu verhindern.", - "window_minimum_open_time": "Die Mindestzeit, die ein Fenster geöffnet sein muss, bevor das System reagiert.", - "sensor_max_value_age": "Das maximale Alter des Temperatursensorwerts, bevor er als gestoppt betrachtet wird." - } - }, - "advanced": { - "title": "Erweitert", - "data": { - "simulation": "Simulation", - "sample_time": "Abtastzeit", - "thermal_comfort": "Thermischer Komfort", - "minimum_consumption": "Mindestverbrauch", - "maximum_consumption": "Maximalverbrauch", - "climate_valve_offset": "Klimaventil-Offset", - "target_temperature_step": "Schrittweite der Zieltemperatur", - "maximum_relative_modulation": "Maximale relative Modulation", - "force_pulse_width_modulation": "Pulsweitenmodulation erzwingen", - "dynamic_minimum_setpoint": "Dynamischer Minimaler Sollwert (Experimentell)" + "sensor_max_value_age": "Das maximale Alter des Temperatursensorwerts, bevor er als gestoppt betrachtet wird.", + "window_minimum_open_time": "Die Mindestzeit, die ein Fenster geöffnet sein muss, bevor das System reagiert." }, - "data_description": { - "thermal_comfort": "Verwendung des Simmer-Index für die Anpassung des thermischen Komforts aktivieren.", - "minimum_consumption": "Der minimale Gasverbrauch, wenn der Kessel aktiv ist.", - "maximum_consumption": "Der maximale Gasverbrauch, wenn der Kessel aktiv ist.", - "climate_valve_offset": "Offset zur Anpassung des Öffnungsgrads des Klimaventils.", - "target_temperature_step": "Die Schrittweite der Zieltemperatur zur Feinabstimmung der Komfortstufen anpassen.", - "sample_time": "Das minimale Zeitintervall zwischen Aktualisierungen des PID-Reglers.", - "maximum_relative_modulation": "Die höchste Modulationsstufe für ein effizientes Heizsystem darstellen." - } + "description": "Für Feinabstimmung und Anpassung.", + "title": "Systemkonfiguration" } } } -} +} \ No newline at end of file diff --git a/custom_components/sat/translations/en.json b/custom_components/sat/translations/en.json index cfee4b0b..54b20138 100644 --- a/custom_components/sat/translations/en.json +++ b/custom_components/sat/translations/en.json @@ -1,224 +1,225 @@ { "config": { + "abort": { + "already_configured": "Gateway is already configured." + }, + "error": { + "connection": "Unable to connect to the gateway.", + "mqtt_component": "The MQTT component is unavailable.", + "unable_to_calibrate": "The calibration process has encountered an issue and could not be completed successfully. Please ensure that your heating system is functioning properly and that all required sensors are connected and working correctly.\n\nIf you continue to experience issues with calibration, consider contacting us for further assistance. We apologize for any inconvenience caused." + }, + "progress": { + "calibration": "Calibrating and finding the overshoot protection value...\n\nPlease wait while we optimize your heating system. This process may take approximately 20 minutes." + }, "step": { - "user": { - "title": "Smart Autotune Thermostat (SAT)", - "description": "SAT is a smart thermostat that is capable of auto-tuning itself to optimize temperature control. Select the appropriate mode that matches your heating system.", - "menu_options": { - "mosquitto": "OpenTherm Gateway ( MQTT )", - "serial": "OpenTherm Gateway ( SERIAL )", - "switch": "PID Thermostat with PWM ( ON/OFF )", - "simulator": "Simulated Gateway ( ADVANCED )" - } - }, - "mosquitto": { - "title": "OpenTherm Gateway ( MQTT )", - "description": "Please provide the following details to set up the OpenTherm Gateway. In the Name field, enter a name for the gateway that will help you identify it within your system.\n\nSpecify the Climate entity to use for the OpenTherm Gateway. This entity is provided by the OpenTherm Gateway and represents your heating system.\n\nAdditionally, enter the Top Topic that will be used for publishing and subscribing to MQTT messages related to the OpenTherm Gateway.\n\nThese settings are essential for establishing communication and integration with your OpenTherm Gateway through MQTT. They allow for seamless data exchange and control of your heating system. Ensure that the provided details are accurate to ensure proper functionality.", - "data": { - "name": "Name", - "device": "Device", - "mqtt_topic": "Top Topic" - } - }, - "serial": { - "title": "OpenTherm Gateway ( SERIAL )", - "description": "To establish a connection with the OpenTherm Gateway using a socket connection, please provide the following details. In the Name field, enter a name for the gateway that will help you identify it within your system.\n\nSpecify the network address of the OpenTherm Gateway in the Device field. This could be in the format of \"socket://otgw.local:25238\", where \"otgw.local\" is the hostname or IP address of the gateway and \"25238\" is the port number.\n\nThese settings are essential for establishing communication and integration with your OpenTherm Gateway through the socket connection. Ensure that the provided details are accurate to ensure proper functionality.", - "data": { - "name": "Name", - "device": "URL" - } - }, - "switch": { - "title": "PID Thermostat with PWM ( ON/OFF )", - "description": "Please fill in the following details to set up the switch. Enter a name for the switch in the Name field, which will help you identify it within your system. Choose the appropriate entity to use for your switch from the provided options.\n\nIn the Temperature Setting field, specify the desired target temperature for your heating system. If you are using a hot water boiler, fill in the Boiler Temperature Setting with the appropriate value. For electric heating systems, enter the value 100.\n\nThese settings are essential for precise temperature control and ensuring optimal performance of your heating system. Providing the correct Temperature Setting allows for accurate regulation and helps achieve a comfortable and energy-efficient environment in your home.", - "data": { - "name": "Name", - "device": "Entity", - "minimum_setpoint": "Temperature Setting" - } - }, - "simulator": { - "title": "Simulated Gateway ( ADVANCED )", - "description": "This gateway allows you to simulate a boiler for testing and demonstration purposes. Please provide the following information to configure the simulator.\n\nNote: The Simulator Gateway is intended for testing and demonstration purposes only and should not be used in production environments.", - "data": { - "name": "Name", - "minimum_setpoint": "Minimum Setpoint", - "maximum_setpoint": "Maximum Setpoint", - "simulated_heating": "Simulated Heating", - "simulated_cooling": "Simulated Cooling", - "simulated_warming_up": "Simulated Warming Up" - } - }, - "sensors": { - "title": "Configure sensors", - "description": "Please select the sensors that will be used to track the temperature.", - "data": { - "inside_sensor_entity_id": "Inside Sensor Entity", - "outside_sensor_entity_id": "Outside Sensor Entity", - "humidity_sensor_entity_id": "Humidity Sensor Entity" - } - }, - "heating_system": { - "title": "Heating System", - "description": "Selecting the correct heating system type is important for SAT to accurately control the temperature and optimize performance. Choose the option that matches your setup to ensure proper temperature regulation throughout your home.", - "data": { - "heating_system": "System" - } - }, "areas": { - "title": "Areas", - "description": "Settings related to climates, multi-room and temperature control. Primary climates are in the same room as the inside sensor and the rooms have their own target temperatures separate from the system.", "data": { "main_climates": "Primary", "secondary_climates": "Rooms" - } + }, + "description": "Settings related to climates, multi-room and temperature control. Primary climates are in the same room as the inside sensor and the rooms have their own target temperatures separate from the system.", + "title": "Areas" }, "automatic_gains": { - "title": "Automatic Gains", - "description": "This feature adjusts the control parameters of your heating system dynamically, optimizing temperature control for better comfort and energy efficiency. Enabling this option allows SAT to continuously adapt and fine-tune the heating settings based on the environmental conditions. This helps maintain a stable and comfortable environment without manual intervention.\n\nNote: If you choose not to enable automatic gains, you will need to manually enter the PID values for precise temperature control. Please ensure that you have accurate PID values for your specific heating system to achieve optimal performance.", "data": { "automatic_gains": "Automatic Gains (recommended)" - } + }, + "description": "This feature adjusts the control parameters of your heating system dynamically, optimizing temperature control for better comfort and energy efficiency. Enabling this option allows SAT to continuously adapt and fine-tune the heating settings based on the environmental conditions. This helps maintain a stable and comfortable environment without manual intervention.\n\nNote: If you choose not to enable automatic gains, you will need to manually enter the PID values for precise temperature control. Please ensure that you have accurate PID values for your specific heating system to achieve optimal performance.", + "title": "Automatic Gains" }, "calibrate_system": { - "title": "Calibrate System", "description": "Optimize your heating system by automatically determining the optimal PID values for your setup. When selecting Automatic Gains, please note that the system will go through a calibration process that may take approximately 20 minutes to complete.\n\nAutomatic Gains is recommended for most users as it simplifies the setup process and ensures optimal performance. However, if you're familiar with PID control and prefer to manually set the values, you can choose to skip Automatic Gains.\n\nPlease note that choosing to skip Automatic Gains requires a good understanding of PID control and may require additional manual adjustments to achieve optimal performance.", "menu_options": { "calibrate": "Calibrate and determine your overshoot protection value (approx. 20 min).", "overshoot_protection": "Manually enter the overshoot protection value.", "pid_controller": "Manually enter PID values (not recommended)." - } + }, + "title": "Calibrate System" + }, + "calibrated": { + "description": "The calibration process has completed successfully.\n\nCongratulations! Your Smart Autotune Thermostat (SAT) has been calibrated to optimize the heating performance of your system. During the calibration process, SAT carefully analyzed the heating characteristics and determined the appropriate overshoot protection value to ensure precise temperature control.\n\nOvershoot Protection Value: {minimum_setpoint} °C\n\nThis value represents the maximum amount of overshoot allowed during the heating process. SAT will actively monitor and adjust the heating to prevent excessive overshooting, maintaining a comfortable and efficient heating experience in your home.\n\nPlease note that the overshoot protection value may vary depending on the specific characteristics of your heating system and environmental factors. It has been fine-tuned to provide optimal performance based on the calibration results.", + "menu_options": { + "calibrate": "Retry calibration", + "finish": "Continue with current calibration" + }, + "title": "Calibration Completed" + }, + "heating_system": { + "data": { + "heating_system": "System" + }, + "description": "Selecting the correct heating system type is important for SAT to accurately control the temperature and optimize performance. Choose the option that matches your setup to ensure proper temperature regulation throughout your home.", + "title": "Heating System" + }, + "mosquitto": { + "data": { + "device": "Device", + "mqtt_topic": "Top Topic", + "name": "Name" + }, + "description": "Please provide the following details to set up the OpenTherm Gateway. In the Name field, enter a name for the gateway that will help you identify it within your system.\n\nSpecify the Climate entity to use for the OpenTherm Gateway. This entity is provided by the OpenTherm Gateway and represents your heating system.\n\nAdditionally, enter the Top Topic that will be used for publishing and subscribing to MQTT messages related to the OpenTherm Gateway.\n\nThese settings are essential for establishing communication and integration with your OpenTherm Gateway through MQTT. They allow for seamless data exchange and control of your heating system. Ensure that the provided details are accurate to ensure proper functionality.", + "title": "OpenTherm Gateway ( MQTT )" }, "overshoot_protection": { - "title": "Overshoot Protection", - "description": "By providing the overshoot protection value, SAT will adjust the control parameters accordingly to maintain a stable and comfortable heating environment. This manual configuration allows you to fine-tune the system based on your specific setup.\n\nNote: If you are unsure about the overshoot protection value or have not performed the calibration process, it is recommended to cancel the configuration and go through the calibration process to let SAT automatically determine the value for optimal performance.", "data": { "minimum_setpoint": "Value" - } + }, + "description": "By providing the overshoot protection value, SAT will adjust the control parameters accordingly to maintain a stable and comfortable heating environment. This manual configuration allows you to fine-tune the system based on your specific setup.\n\nNote: If you are unsure about the overshoot protection value or have not performed the calibration process, it is recommended to cancel the configuration and go through the calibration process to let SAT automatically determine the value for optimal performance.", + "title": "Overshoot Protection" }, "pid_controller": { - "title": "Configure the PID controller manually.", - "description": "Configure the proportional, integral, and derivative gains manually to fine-tune your heating system. Use this option if you prefer to have full control over the PID controller parameters. Adjust the gains based on your specific heating system characteristics and preferences.", "data": { - "integral": "Integral (kI)", "derivative": "Derivative (kD)", + "integral": "Integral (kI)", "proportional": "Proportional (kP)" - } + }, + "description": "Configure the proportional, integral, and derivative gains manually to fine-tune your heating system. Use this option if you prefer to have full control over the PID controller parameters. Adjust the gains based on your specific heating system characteristics and preferences.", + "title": "Configure the PID controller manually." }, - "calibrated": { - "title": "Calibration Completed", - "description": "The calibration process has completed successfully.\n\nCongratulations! Your Smart Autotune Thermostat (SAT) has been calibrated to optimize the heating performance of your system. During the calibration process, SAT carefully analyzed the heating characteristics and determined the appropriate overshoot protection value to ensure precise temperature control.\n\nOvershoot Protection Value: {minimum_setpoint} °C\n\nThis value represents the maximum amount of overshoot allowed during the heating process. SAT will actively monitor and adjust the heating to prevent excessive overshooting, maintaining a comfortable and efficient heating experience in your home.\n\nPlease note that the overshoot protection value may vary depending on the specific characteristics of your heating system and environmental factors. It has been fine-tuned to provide optimal performance based on the calibration results.", + "sensors": { + "data": { + "humidity_sensor_entity_id": "Humidity Sensor Entity", + "inside_sensor_entity_id": "Inside Sensor Entity", + "outside_sensor_entity_id": "Outside Sensor Entity" + }, + "description": "Please select the sensors that will be used to track the temperature.", + "title": "Configure sensors" + }, + "serial": { + "data": { + "device": "URL", + "name": "Name" + }, + "description": "To establish a connection with the OpenTherm Gateway using a socket connection, please provide the following details. In the Name field, enter a name for the gateway that will help you identify it within your system.\n\nSpecify the network address of the OpenTherm Gateway in the Device field. This could be in the format of \"socket://otgw.local:25238\", where \"otgw.local\" is the hostname or IP address of the gateway and \"25238\" is the port number.\n\nThese settings are essential for establishing communication and integration with your OpenTherm Gateway through the socket connection. Ensure that the provided details are accurate to ensure proper functionality.", + "title": "OpenTherm Gateway ( SERIAL )" + }, + "simulator": { + "data": { + "maximum_setpoint": "Maximum Setpoint", + "minimum_setpoint": "Minimum Setpoint", + "name": "Name", + "simulated_cooling": "Simulated Cooling", + "simulated_heating": "Simulated Heating", + "simulated_warming_up": "Simulated Warming Up" + }, + "description": "This gateway allows you to simulate a boiler for testing and demonstration purposes. Please provide the following information to configure the simulator.\n\nNote: The Simulator Gateway is intended for testing and demonstration purposes only and should not be used in production environments.", + "title": "Simulated Gateway ( ADVANCED )" + }, + "switch": { + "data": { + "device": "Entity", + "minimum_setpoint": "Temperature Setting", + "name": "Name" + }, + "description": "Please fill in the following details to set up the switch. Enter a name for the switch in the Name field, which will help you identify it within your system. Choose the appropriate entity to use for your switch from the provided options.\n\nIn the Temperature Setting field, specify the desired target temperature for your heating system. If you are using a hot water boiler, fill in the Boiler Temperature Setting with the appropriate value. For electric heating systems, enter the value 100.\n\nThese settings are essential for precise temperature control and ensuring optimal performance of your heating system. Providing the correct Temperature Setting allows for accurate regulation and helps achieve a comfortable and energy-efficient environment in your home.", + "title": "PID Thermostat with PWM ( ON/OFF )" + }, + "user": { + "description": "SAT is a smart thermostat that is capable of auto-tuning itself to optimize temperature control. Select the appropriate mode that matches your heating system.", "menu_options": { - "calibrate": "Retry calibration", - "finish": "Continue with current calibration" - } + "mosquitto": "OpenTherm Gateway ( MQTT )", + "serial": "OpenTherm Gateway ( SERIAL )", + "simulator": "Simulated Gateway ( ADVANCED )", + "switch": "PID Thermostat with PWM ( ON/OFF )" + }, + "title": "Smart Autotune Thermostat (SAT)" } - }, - "error": { - "connection": "Unable to connect to the gateway.", - "mqtt_component": "The MQTT component is unavailable.", - "unable_to_calibrate": "The calibration process has encountered an issue and could not be completed successfully. Please ensure that your heating system is functioning properly and that all required sensors are connected and working correctly.\n\nIf you continue to experience issues with calibration, consider contacting us for further assistance. We apologize for any inconvenience caused." - }, - "abort": { - "already_configured": "Gateway is already configured." - }, - "progress": { - "calibration": "Calibrating and finding the overshoot protection value...\n\nPlease wait while we optimize your heating system. This process may take approximately 20 minutes." } }, "options": { "step": { - "init": { - "menu_options": { - "general": "General", - "presets": "Presets", - "advanced": "Advanced Options", - "system_configuration": "System Configuration" - } + "advanced": { + "data": { + "climate_valve_offset": "Climate valve offset", + "dynamic_minimum_setpoint": "Dynamic Minimum Setpoint (Experimental)", + "force_pulse_width_modulation": "Force Pulse Width Modulation", + "maximum_consumption": "Maximum Consumption", + "maximum_relative_modulation": "Maximum Relative Modulation", + "minimum_consumption": "Minimum Consumption", + "sample_time": "Sample Time", + "simulation": "Simulation", + "target_temperature_step": "Target Temperature Step", + "thermal_comfort": "Thermal Comfort" + }, + "data_description": { + "climate_valve_offset": "Offset to adjust the opening degree of the climate valve.", + "dynamic_minimum_setpoint": "Activates the dynamic setpoint adjustment based on the boiler's return temperature, which also helps identify if any valves are closed.", + "maximum_consumption": "The maximum gas consumption when the boiler is active.", + "maximum_relative_modulation": "Representing the highest modulation level for an efficient heating system.", + "minimum_consumption": "The minimum gas consumption when the boiler is active.", + "sample_time": "The minimum time interval between updates to the PID controller.", + "target_temperature_step": "Adjust the target temperature step for fine-tuning comfort levels.", + "thermal_comfort": "Enable the use of the Simmer Index for thermal comfort adjustment." + }, + "title": "Advanced" }, "general": { - "title": "General", - "description": "General settings and configurations.", "data": { - "integral": "Integral (kI)", - "derivative": "Derivative (kD)", - "proportional": "Proportional (kP)", - "maximum_setpoint": "Maximum Setpoint", - "window_sensors": "Contact Sensors", "automatic_gains_value": "Automatic Gains Value", + "derivative": "Derivative (kD)", "derivative_time_weight": "Derivative Time Weight", - "heating_curve_version": "Heating Curve Version", - "heating_curve_coefficient": "Heating Curve Coefficient", "duty_cycle": "Maximum Duty Cycle for Pulse Width Modulation", + "heating_curve_coefficient": "Heating Curve Coefficient", + "heating_curve_version": "Heating Curve Version", + "integral": "Integral (kI)", + "maximum_setpoint": "Maximum Setpoint", + "minimum_setpoint_adjustment_factor": "Adjustment Factor for Return Temperature", + "pid_controller_version": "PID Controller Version", + "proportional": "Proportional (kP)", "sync_with_thermostat": "Synchronize setpoint with thermostat", - "minimum_setpoint_adjustment_factor": "Adjustment Factor for Return Temperature" + "window_sensors": "Contact Sensors" }, "data_description": { - "integral": "The integral term (kI) in the PID controller, responsible for reducing steady-state error.", - "derivative": "The derivative term (kD) in the PID controller, responsible for mitigating overshooting.", - "proportional": "The proportional term (kP) in the PID controller, responsible for the immediate response to errors.", - "maximum_setpoint": "The optimal temperature for efficient boiler operation.", - "window_sensors": "Contact Sensors that trigger the system to react when a window or door is open for a period of time.", "automatic_gains_value": "The value used for automatic gains in the PID controller.", - "heating_curve_coefficient": "The coefficient used to adjust the heating curve.", + "derivative": "The derivative term (kD) in the PID controller, responsible for mitigating overshooting.", + "derivative_time_weight": "A parameter to adjust the influence of the derivative term over time, particularly useful for reducing undershoot during the warm-up phase when the heating curve coefficient is correctly set.", "duty_cycle": "The maximum duty cycle for Pulse Width Modulation (PWM), controlling the boiler's on-off cycles.", + "heating_curve_coefficient": "The coefficient used to adjust the heating curve.", + "integral": "The integral term (kI) in the PID controller, responsible for reducing steady-state error.", + "maximum_setpoint": "The optimal temperature for efficient boiler operation.", + "minimum_setpoint_adjustment_factor": "This factor adjusts the heating setpoint based on the boiler's return temperature, affecting heating responsiveness and efficiency. A higher value increases sensitivity to temperature changes, enhancing control over comfort and energy use. Recommended starting range is 0.1 to 0.5. Adjust to suit your system and comfort preferences.", + "proportional": "The proportional term (kP) in the PID controller, responsible for the immediate response to errors.", "sync_with_thermostat": "Synchronize setpoint with thermostat to ensure coordinated temperature control.", - "derivative_time_weight": "A parameter to adjust the influence of the derivative term over time, particularly useful for reducing undershoot during the warm-up phase when the heating curve coefficient is correctly set.", - "minimum_setpoint_adjustment_factor": "This factor adjusts the heating setpoint based on the boiler's return temperature, affecting heating responsiveness and efficiency. A higher value increases sensitivity to temperature changes, enhancing control over comfort and energy use. Recommended starting range is 0.1 to 0.5. Adjust to suit your system and comfort preferences." + "window_sensors": "Contact Sensors that trigger the system to react when a window or door is open for a period of time." + }, + "description": "General settings and configurations.", + "title": "General" + }, + "init": { + "menu_options": { + "advanced": "Advanced Options", + "general": "General", + "presets": "Presets", + "system_configuration": "System Configuration" } }, "presets": { - "title": "Presets", - "description": "Predefined temperature settings for different scenarios or activities.", "data": { + "activity_temperature": "Activity Temperature", "away_temperature": "Away Temperature", + "comfort_temperature": "Comfort Temperature", "home_temperature": "Home Temperature", "sleep_temperature": "Sleep Temperature", - "comfort_temperature": "Comfort Temperature", - "activity_temperature": "Activity Temperature", "sync_climates_with_preset": "Synchronize climates with preset (sleep / away / activity)" - } + }, + "description": "Predefined temperature settings for different scenarios or activities.", + "title": "Presets" }, "system_configuration": { - "title": "System Configuration", - "description": "For fine-tuning and customization.", "data": { "automatic_duty_cycle": "Automatic duty cycle", "overshoot_protection": "Overshoot Protection (with PWM)", - "window_minimum_open_time": "Minimum time for window to be open", - "sensor_max_value_age": "Temperature Sensor maximum value age" + "sensor_max_value_age": "Temperature Sensor maximum value age", + "window_minimum_open_time": "Minimum time for window to be open" }, "data_description": { "automatic_duty_cycle": "Enable or disable automatic duty cycle for Pulse Width Modulation (PWM).", "overshoot_protection": "Enable overshoot protection with Pulse Width Modulation (PWM) to prevent boiler temperature overshooting.", - "window_minimum_open_time": "The minimum time a window must be open before the system reacts.", - "sensor_max_value_age": "The maximum age of the temperature sensor value before considering it as a stall." - } - }, - "advanced": { - "title": "Advanced", - "data": { - "simulation": "Simulation", - "sample_time": "Sample Time", - "thermal_comfort": "Thermal Comfort", - "minimum_consumption": "Minimum Consumption", - "maximum_consumption": "Maximum Consumption", - "climate_valve_offset": "Climate valve offset", - "target_temperature_step": "Target Temperature Step", - "maximum_relative_modulation": "Maximum Relative Modulation", - "force_pulse_width_modulation": "Force Pulse Width Modulation", - "dynamic_minimum_setpoint": "Dynamic Minimum Setpoint (Experimental)" + "sensor_max_value_age": "The maximum age of the temperature sensor value before considering it as a stall.", + "window_minimum_open_time": "The minimum time a window must be open before the system reacts." }, - "data_description": { - "thermal_comfort": "Enable the use of the Simmer Index for thermal comfort adjustment.", - "minimum_consumption": "The minimum gas consumption when the boiler is active.", - "maximum_consumption": "The maximum gas consumption when the boiler is active.", - "climate_valve_offset": "Offset to adjust the opening degree of the climate valve.", - "target_temperature_step": "Adjust the target temperature step for fine-tuning comfort levels.", - "sample_time": "The minimum time interval between updates to the PID controller.", - "maximum_relative_modulation": "Representing the highest modulation level for an efficient heating system.", - "dynamic_minimum_setpoint": "Activates the dynamic setpoint adjustment based on the boiler's return temperature, which also helps identify if any valves are closed." - } + "description": "For fine-tuning and customization.", + "title": "System Configuration" } } } diff --git a/custom_components/sat/translations/es.json b/custom_components/sat/translations/es.json index 1be1bb73..21d4ec97 100644 --- a/custom_components/sat/translations/es.json +++ b/custom_components/sat/translations/es.json @@ -1,221 +1,225 @@ { "config": { + "abort": { + "already_configured": "La puerta de enlace ya está configurada." + }, + "error": { + "connection": "No se puede conectar a la puerta de enlace.", + "mqtt_component": "El componente MQTT no está disponible.", + "unable_to_calibrate": "El proceso de calibración ha encontrado un problema y no se pudo completar con éxito. Por favor, asegúrese de que su sistema de calefacción está funcionando correctamente y que todos los sensores necesarios están conectados y funcionando correctamente.\n\nSi continúa experimentando problemas con la calibración, considere contactarnos para obtener más ayuda. Pedimos disculpas por cualquier inconveniente causado." + }, + "progress": { + "calibration": "Calibrando y encontrando el valor de protección contra sobrecalentamiento...\n\nPor favor, espere mientras optimizamos su sistema de calefacción. Este proceso puede tomar aproximadamente 20 minutos." + }, "step": { - "user": { - "title": "Smart Autotune Thermostat (SAT)", - "description": "SAT es un termostato inteligente capaz de autoajustarse para optimizar el control de la temperatura. Seleccione el modo apropiado que coincida con su sistema de calefacción.", - "menu_options": { - "mosquitto": "Puerta de Enlace OpenTherm (MQTT)", - "serial": "Puerta de Enlace OpenTherm (SERIAL)", - "switch": "Termostato PID con PWM (ON/OFF)", - "simulator": "Puerta de Enlace Simulada (AVANZADO)" - } - }, - "mosquitto": { - "title": "Puerta de Enlace OpenTherm (MQTT)", - "description": "Proporcione los siguientes detalles para configurar la Puerta de Enlace OpenTherm. En el campo Nombre, introduzca un nombre para la puerta de enlace que le ayude a identificarla dentro de su sistema.\n\nEspecifique la entidad Climática que usará para la Puerta de Enlace OpenTherm. Esta entidad es proporcionada por la Puerta de Enlace OpenTherm y representa su sistema de calefacción.\n\nAdicionalmente, introduzca el Tema Principal que se usará para publicar y suscribirse a mensajes MQTT relacionados con la Puerta de Enlace OpenTherm.\n\nEstos ajustes son esenciales para establecer la comunicación e integración con su Puerta de Enlace OpenTherm a través de MQTT. Permiten un intercambio de datos sin problemas y el control de su sistema de calefacción. Asegúrese de que los detalles proporcionados sean precisos para garantizar una funcionalidad adecuada.", - "data": { - "name": "Nombre", - "device": "Dispositivo", - "mqtt_topic": "Tema Principal" - } - }, - "serial": { - "title": "Puerta de Enlace OpenTherm (SERIAL)", - "description": "Para establecer una conexión con la Puerta de Enlace OpenTherm usando una conexión de socket, por favor proporcione los siguientes detalles. En el campo Nombre, introduzca un nombre para la puerta de enlace que le ayude a identificarla dentro de su sistema.\n\nEspecifique la dirección de red de la Puerta de Enlace OpenTherm en el campo Dispositivo. Esto podría estar en el formato de \"socket://otgw.local:25238\", donde \"otgw.local\" es el nombre de host o la dirección IP de la puerta de enlace y \"25238\" es el número de puerto.\n\nEstos ajustes son esenciales para establecer la comunicación e integración con su Puerta de Enlace OpenTherm a través de la conexión de socket. Asegúrese de que los detalles proporcionados sean precisos para garantizar una funcionalidad adecuada.", - "data": { - "name": "Nombre", - "device": "URL" - } - }, - "switch": { - "title": "Termostato PID con PWM (ON/OFF)", - "description": "Por favor, rellene los siguientes detalles para configurar el interruptor. Introduzca un nombre para el interruptor en el campo Nombre, lo que le ayudará a identificarlo dentro de su sistema. Elija la entidad adecuada para usar con su interruptor de las opciones proporcionadas.\n\nEn el campo de Ajuste de Temperatura, especifique la temperatura objetivo deseada para su sistema de calefacción. Si está usando una caldera de agua caliente, rellene el Ajuste de Temperatura de la Caldera con el valor adecuado. Para sistemas de calefacción eléctrica, introduzca el valor 100.\n\nEstos ajustes son esenciales para un control preciso de la temperatura y para asegurar un rendimiento óptimo de su sistema de calefacción. Proporcionar el Ajuste de Temperatura correcto permite una regulación precisa y ayuda a lograr un ambiente cómodo y eficiente en energía en su hogar.", - "data": { - "name": "Nombre", - "device": "Entidad", - "minimum_setpoint": "Ajuste de Temperatura" - } - }, - "simulator": { - "title": "Puerta de Enlace Simulada (AVANZADO)", - "description": "Esta puerta de enlace le permite simular una caldera para propósitos de pruebas y demostración. Por favor, proporcione la siguiente información para configurar el simulador.\n\nNota: La Puerta de Enlace Simulada está destinada solo para pruebas y propósitos de demostración y no debería usarse en entornos de producción.", - "data": { - "name": "Nombre", - "minimum_setpoint": "Ajuste Mínimo", - "maximum_setpoint": "Ajuste Máximo", - "simulated_heating": "Calefacción Simulada", - "simulated_cooling": "Enfriamiento Simulado", - "simulated_warming_up": "Calentamiento Simulado" - } - }, - "sensors": { - "title": "Configurar sensores", - "description": "Por favor, seleccione los sensores que se usarán para rastrear la temperatura.", - "data": { - "inside_sensor_entity_id": "Entidad del Sensor Interior", - "outside_sensor_entity_id": "Entidad del Sensor Exterior", - "humidity_sensor_entity_id": "Entidad del Sensor de Humedad" - } - }, - "heating_system": { - "title": "Sistema de Calefacción", - "description": "Seleccionar el tipo correcto de sistema de calefacción es importante para que SAT controle la temperatura de manera precisa y optimice el rendimiento. Elija la opción que coincida con su configuración para asegurar una regulación adecuada de la temperatura en todo su hogar.", - "data": { - "heating_system": "Sistema" - } - }, "areas": { - "title": "Áreas", - "description": "Configuraciones relacionadas con climas, múltiples habitaciones y control de temperatura. Los climas principales están en la misma habitación que el sensor interior y las habitaciones tienen sus propias temperaturas objetivo separadas del sistema.", "data": { "main_climates": "Primarios", "secondary_climates": "Habitaciones" - } + }, + "description": "Configuraciones relacionadas con climas, múltiples habitaciones y control de temperatura. Los climas principales están en la misma habitación que el sensor interior y las habitaciones tienen sus propias temperaturas objetivo separadas del sistema.", + "title": "Áreas" }, "automatic_gains": { - "title": "Ganancias Automáticas", - "description": "Esta característica ajusta los parámetros de control de su sistema de calefacción de forma dinámica, optimizando el control de la temperatura para mayor comodidad y eficiencia energética. Habilitar esta opción permite que SAT se adapte y ajuste finamente los ajustes de calefacción basándose en las condiciones ambientales. Esto ayuda a mantener un ambiente estable y cómodo sin intervención manual.\n\nNota: Si elige no habilitar las ganancias automáticas, necesitará introducir manualmente los valores PID para un control preciso de la temperatura. Por favor, asegúrese de tener valores PID precisos para su sistema de calefacción específico para lograr un rendimiento óptimo.", "data": { "automatic_gains": "Ganancias Automáticas (recomendado)" - } + }, + "description": "Esta característica ajusta los parámetros de control de su sistema de calefacción de forma dinámica, optimizando el control de la temperatura para mayor comodidad y eficiencia energética. Habilitar esta opción permite que SAT se adapte y ajuste finamente los ajustes de calefacción basándose en las condiciones ambientales. Esto ayuda a mantener un ambiente estable y cómodo sin intervención manual.\n\nNota: Si elige no habilitar las ganancias automáticas, necesitará introducir manualmente los valores PID para un control preciso de la temperatura. Por favor, asegúrese de tener valores PID precisos para su sistema de calefacción específico para lograr un rendimiento óptimo.", + "title": "Ganancias Automáticas" }, "calibrate_system": { - "title": "Calibrar Sistema", "description": "Optimice su sistema de calefacción determinando automáticamente los valores PID óptimos para su configuración. Al seleccionar Ganancias Automáticas, tenga en cuenta que el sistema pasará por un proceso de calibración que puede tardar aproximadamente 20 minutos en completarse.\n\nSe recomienda Ganancias Automáticas para la mayoría de los usuarios, ya que simplifica el proceso de configuración y asegura un rendimiento óptimo. Sin embargo, si está familiarizado con el control PID y prefiere configurar los valores manualmente, puede optar por omitir las Ganancias Automáticas.\n\nTenga en cuenta que elegir omitir las Ganancias Automáticas requiere un buen entendimiento del control PID y puede requerir ajustes manuales adicionales para lograr un rendimiento óptimo.", "menu_options": { "calibrate": "Calibrar y determinar su valor de protección contra sobrecalentamiento (aprox. 20 min).", "overshoot_protection": "Introducir manualmente el valor de protección contra sobrecalentamiento.", "pid_controller": "Introducir manualmente los valores PID (no recomendado)." - } + }, + "title": "Calibrar Sistema" + }, + "calibrated": { + "description": "El proceso de calibración se ha completado con éxito.\n\n¡Felicitaciones! Su Smart Autotune Thermostat (SAT) ha sido calibrado para optimizar el rendimiento de calefacción de su sistema. Durante el proceso de calibración, SAT analizó cuidadosamente las características de calefacción y determinó el valor de protección contra sobrecalentamiento adecuado para asegurar un control preciso de la temperatura.\n\nValor de Protección contra Sobrecalentamiento: {minimum_setpoint} °C\n\nEste valor representa la cantidad máxima de sobrecalentamiento permitido durante el proceso de calefacción. SAT monitorizará activamente y ajustará la calefacción para prevenir un sobrecalentamiento excesivo, manteniendo una experiencia de calefacción cómoda y eficiente en su hogar.\n\nTenga en cuenta que el valor de protección contra sobrecalentamiento puede variar dependiendo de las características específicas de su sistema de calefacción y factores ambientales. Ha sido ajustado finamente para proporcionar un rendimiento óptimo basado en los resultados de calibración.", + "menu_options": { + "calibrate": "Reintentar calibración", + "finish": "Continuar con la calibración actual" + }, + "title": "Calibración Completada" + }, + "heating_system": { + "data": { + "heating_system": "Sistema" + }, + "description": "Seleccionar el tipo correcto de sistema de calefacción es importante para que SAT controle la temperatura de manera precisa y optimice el rendimiento. Elija la opción que coincida con su configuración para asegurar una regulación adecuada de la temperatura en todo su hogar.", + "title": "Sistema de Calefacción" + }, + "mosquitto": { + "data": { + "device": "Dispositivo", + "mqtt_topic": "Tema Principal", + "name": "Nombre" + }, + "description": "Proporcione los siguientes detalles para configurar la Puerta de Enlace OpenTherm. En el campo Nombre, introduzca un nombre para la puerta de enlace que le ayude a identificarla dentro de su sistema.\n\nEspecifique la entidad Climática que usará para la Puerta de Enlace OpenTherm. Esta entidad es proporcionada por la Puerta de Enlace OpenTherm y representa su sistema de calefacción.\n\nAdicionalmente, introduzca el Tema Principal que se usará para publicar y suscribirse a mensajes MQTT relacionados con la Puerta de Enlace OpenTherm.\n\nEstos ajustes son esenciales para establecer la comunicación e integración con su Puerta de Enlace OpenTherm a través de MQTT. Permiten un intercambio de datos sin problemas y el control de su sistema de calefacción. Asegúrese de que los detalles proporcionados sean precisos para garantizar una funcionalidad adecuada.", + "title": "Puerta de Enlace OpenTherm (MQTT)" }, "overshoot_protection": { - "title": "Protección contra Sobrecalentamiento", - "description": "Al proporcionar el valor de protección contra sobrecalentamiento, SAT ajustará los parámetros de control en consecuencia para mantener un ambiente de calefacción estable y cómodo. Esta configuración manual le permite ajustar el sistema en base a su configuración específica.\n\nNota: Si no está seguro sobre el valor de protección contra sobrecalentamiento o no ha realizado el proceso de calibración, se recomienda cancelar la configuración y pasar por el proceso de calibración para permitir que SAT determine automáticamente el valor para un rendimiento óptimo.", "data": { "minimum_setpoint": "Valor" - } + }, + "description": "Al proporcionar el valor de protección contra sobrecalentamiento, SAT ajustará los parámetros de control en consecuencia para mantener un ambiente de calefacción estable y cómodo. Esta configuración manual le permite ajustar el sistema en base a su configuración específica.\n\nNota: Si no está seguro sobre el valor de protección contra sobrecalentamiento o no ha realizado el proceso de calibración, se recomienda cancelar la configuración y pasar por el proceso de calibración para permitir que SAT determine automáticamente el valor para un rendimiento óptimo.", + "title": "Protección contra Sobrecalentamiento" }, "pid_controller": { - "title": "Configurar manualmente el controlador PID.", - "description": "Configure los parámetros de ganancia proporcional, integral y derivativa manualmente para ajustar finamente su sistema de calefacción. Utilice esta opción si prefiere tener control total sobre los parámetros del controlador PID. Ajuste las ganancias basándose en las características específicas de su sistema de calefacción y preferencias.", "data": { - "integral": "Integral (kI)", "derivative": "Derivativo (kD)", + "integral": "Integral (kI)", "proportional": "Proporcional (kP)" - } + }, + "description": "Configure los parámetros de ganancia proporcional, integral y derivativa manualmente para ajustar finamente su sistema de calefacción. Utilice esta opción si prefiere tener control total sobre los parámetros del controlador PID. Ajuste las ganancias basándose en las características específicas de su sistema de calefacción y preferencias.", + "title": "Configurar manualmente el controlador PID." }, - "calibrated": { - "title": "Calibración Completada", - "description": "El proceso de calibración se ha completado con éxito.\n\n¡Felicitaciones! Su Smart Autotune Thermostat (SAT) ha sido calibrado para optimizar el rendimiento de calefacción de su sistema. Durante el proceso de calibración, SAT analizó cuidadosamente las características de calefacción y determinó el valor de protección contra sobrecalentamiento adecuado para asegurar un control preciso de la temperatura.\n\nValor de Protección contra Sobrecalentamiento: {minimum_setpoint} °C\n\nEste valor representa la cantidad máxima de sobrecalentamiento permitido durante el proceso de calefacción. SAT monitorizará activamente y ajustará la calefacción para prevenir un sobrecalentamiento excesivo, manteniendo una experiencia de calefacción cómoda y eficiente en su hogar.\n\nTenga en cuenta que el valor de protección contra sobrecalentamiento puede variar dependiendo de las características específicas de su sistema de calefacción y factores ambientales. Ha sido ajustado finamente para proporcionar un rendimiento óptimo basado en los resultados de calibración.", + "sensors": { + "data": { + "humidity_sensor_entity_id": "Entidad del Sensor de Humedad", + "inside_sensor_entity_id": "Entidad del Sensor Interior", + "outside_sensor_entity_id": "Entidad del Sensor Exterior" + }, + "description": "Por favor, seleccione los sensores que se usarán para rastrear la temperatura.", + "title": "Configurar sensores" + }, + "serial": { + "data": { + "device": "URL", + "name": "Nombre" + }, + "description": "Para establecer una conexión con la Puerta de Enlace OpenTherm usando una conexión de socket, por favor proporcione los siguientes detalles. En el campo Nombre, introduzca un nombre para la puerta de enlace que le ayude a identificarla dentro de su sistema.\n\nEspecifique la dirección de red de la Puerta de Enlace OpenTherm en el campo Dispositivo. Esto podría estar en el formato de \"socket://otgw.local:25238\", donde \"otgw.local\" es el nombre de host o la dirección IP de la puerta de enlace y \"25238\" es el número de puerto.\n\nEstos ajustes son esenciales para establecer la comunicación e integración con su Puerta de Enlace OpenTherm a través de la conexión de socket. Asegúrese de que los detalles proporcionados sean precisos para garantizar una funcionalidad adecuada.", + "title": "Puerta de Enlace OpenTherm (SERIAL)" + }, + "simulator": { + "data": { + "maximum_setpoint": "Ajuste Máximo", + "minimum_setpoint": "Ajuste Mínimo", + "name": "Nombre", + "simulated_cooling": "Enfriamiento Simulado", + "simulated_heating": "Calefacción Simulada", + "simulated_warming_up": "Calentamiento Simulado" + }, + "description": "Esta puerta de enlace le permite simular una caldera para propósitos de pruebas y demostración. Por favor, proporcione la siguiente información para configurar el simulador.\n\nNota: La Puerta de Enlace Simulada está destinada solo para pruebas y propósitos de demostración y no debería usarse en entornos de producción.", + "title": "Puerta de Enlace Simulada (AVANZADO)" + }, + "switch": { + "data": { + "device": "Entidad", + "minimum_setpoint": "Ajuste de Temperatura", + "name": "Nombre" + }, + "description": "Por favor, rellene los siguientes detalles para configurar el interruptor. Introduzca un nombre para el interruptor en el campo Nombre, lo que le ayudará a identificarlo dentro de su sistema. Elija la entidad adecuada para usar con su interruptor de las opciones proporcionadas.\n\nEn el campo de Ajuste de Temperatura, especifique la temperatura objetivo deseada para su sistema de calefacción. Si está usando una caldera de agua caliente, rellene el Ajuste de Temperatura de la Caldera con el valor adecuado. Para sistemas de calefacción eléctrica, introduzca el valor 100.\n\nEstos ajustes son esenciales para un control preciso de la temperatura y para asegurar un rendimiento óptimo de su sistema de calefacción. Proporcionar el Ajuste de Temperatura correcto permite una regulación precisa y ayuda a lograr un ambiente cómodo y eficiente en energía en su hogar.", + "title": "Termostato PID con PWM (ON/OFF)" + }, + "user": { + "description": "SAT es un termostato inteligente capaz de autoajustarse para optimizar el control de la temperatura. Seleccione el modo apropiado que coincida con su sistema de calefacción.", "menu_options": { - "calibrate": "Reintentar calibración", - "finish": "Continuar con la calibración actual" - } + "mosquitto": "Puerta de Enlace OpenTherm (MQTT)", + "serial": "Puerta de Enlace OpenTherm (SERIAL)", + "simulator": "Puerta de Enlace Simulada (AVANZADO)", + "switch": "Termostato PID con PWM (ON/OFF)" + }, + "title": "Smart Autotune Thermostat (SAT)" } - }, - "error": { - "connection": "No se puede conectar a la puerta de enlace.", - "mqtt_component": "El componente MQTT no está disponible.", - "unable_to_calibrate": "El proceso de calibración ha encontrado un problema y no se pudo completar con éxito. Por favor, asegúrese de que su sistema de calefacción está funcionando correctamente y que todos los sensores necesarios están conectados y funcionando correctamente.\n\nSi continúa experimentando problemas con la calibración, considere contactarnos para obtener más ayuda. Pedimos disculpas por cualquier inconveniente causado." - }, - "abort": { - "already_configured": "La puerta de enlace ya está configurada." - }, - "progress": { - "calibration": "Calibrando y encontrando el valor de protección contra sobrecalentamiento...\n\nPor favor, espere mientras optimizamos su sistema de calefacción. Este proceso puede tomar aproximadamente 20 minutos." } }, "options": { "step": { - "init": { - "menu_options": { - "general": "General", - "presets": "Preajustes", - "advanced": "Opciones Avanzadas", - "system_configuration": "Configuración del Sistema" - } + "advanced": { + "data": { + "climate_valve_offset": "Compensación de la Válvula Climática", + "dynamic_minimum_setpoint": "Punto de Ajuste Mínimo Dinámico (Experimental)", + "force_pulse_width_modulation": "Forzar Modulación de Ancho de Pulso", + "maximum_consumption": "Consumo Máximo", + "maximum_relative_modulation": "Modulación Relativa Máxima", + "minimum_consumption": "Consumo Mínimo", + "sample_time": "Tiempo de Muestreo", + "simulation": "Simulación", + "target_temperature_step": "Paso de Temperatura Objetivo", + "thermal_comfort": "Confort Térmico" + }, + "data_description": { + "climate_valve_offset": "Compensación para ajustar el grado de apertura de la válvula climática.", + "dynamic_minimum_setpoint": "Activa el ajuste dinámico del punto de consigna mínimo basado en la temperatura de retorno de la caldera, lo que también ayuda a identificar si alguna válvula está cerrada.", + "maximum_consumption": "El consumo máximo de gas cuando la caldera está activa.", + "maximum_relative_modulation": "Representa el nivel más alto de modulación para un sistema de calefacción eficiente.", + "minimum_consumption": "El consumo mínimo de gas cuando la caldera está activa.", + "sample_time": "El intervalo de tiempo mínimo entre actualizaciones del controlador PID.", + "target_temperature_step": "Ajustar el paso de la temperatura objetivo para una afinación precisa de los niveles de confort.", + "thermal_comfort": "Habilitar el uso del Índice de Simmer para ajuste de confort térmico." + }, + "title": "Avanzadas" }, "general": { - "title": "General", - "description": "Configuraciones y ajustes generales.", "data": { - "integral": "Integral (kI)", - "derivative": "Derivativa (kD)", - "proportional": "Proporcional (kP)", - "maximum_setpoint": "Punto de Ajuste Máximo", - "window_sensors": "Sensores de Contacto", "automatic_gains_value": "Valor de Ganancias Automáticas", + "derivative": "Derivativa (kD)", "derivative_time_weight": "Peso Temporal de la Derivativa", - "heating_curve_version": "Versión de la Curva de Calefacción", - "heating_curve_coefficient": "Coeficiente de la Curva de Calefacción", "duty_cycle": "Ciclo de Trabajo Máximo para la Modulación de Ancho de Pulso", - "sync_with_thermostat": "Sincronizar punto de ajuste con el termostato" + "heating_curve_coefficient": "Coeficiente de la Curva de Calefacción", + "heating_curve_version": "Versión de la Curva de Calefacción", + "integral": "Integral (kI)", + "maximum_setpoint": "Punto de Ajuste Máximo", + "minimum_setpoint_adjustment_factor": "Factor de ajuste del punto de consigna mínimo", + "pid_controller_version": "Versión del controlador PID", + "proportional": "Proporcional (kP)", + "sync_with_thermostat": "Sincronizar punto de ajuste con el termostato", + "window_sensors": "Sensores de Contacto" }, "data_description": { - "integral": "El término integral (kI) en el controlador PID, responsable de reducir el error en estado estacionario.", - "derivative": "El término derivativo (kD) en el controlador PID, responsable de mitigar el sobreimpulso.", - "proportional": "El término proporcional (kP) en el controlador PID, responsable de la respuesta inmediata a errores.", - "maximum_setpoint": "La temperatura óptima para una operación eficiente de la caldera.", - "window_sensors": "Sensores de Contacto que activan el sistema cuando una ventana o puerta está abierta durante un período.", "automatic_gains_value": "El valor utilizado para las ganancias automáticas en el controlador PID.", - "heating_curve_coefficient": "El coeficiente utilizado para ajustar la curva de calefacción.", + "derivative": "El término derivativo (kD) en el controlador PID, responsable de mitigar el sobreimpulso.", + "derivative_time_weight": "Un parámetro para ajustar la influencia del término derivativo a lo largo del tiempo, especialmente útil para reducir el infraimpulso durante la fase de calentamiento cuando el coeficiente de la curva de calefacción está correctamente ajustado.", "duty_cycle": "El ciclo de trabajo máximo para la Modulación de Ancho de Pulso (PWM), controlando los ciclos de encendido/apagado de la caldera.", + "heating_curve_coefficient": "El coeficiente utilizado para ajustar la curva de calefacción.", + "integral": "El término integral (kI) en el controlador PID, responsable de reducir el error en estado estacionario.", + "maximum_setpoint": "La temperatura óptima para una operación eficiente de la caldera.", + "minimum_setpoint_adjustment_factor": "Este factor ajusta el punto de ajuste del calefactor basado en la temperatura de retorno de la caldera, afectando la capacidad de respuesta y eficiencia del calefactor. Un valor más alto aumenta la sensibilidad a los cambios de temperatura, mejorando el control sobre el confort y el uso de energía. El rango inicial recomendado es de 0,1 a 0,5. Ajuste para adaptarse a su sistema y preferencias de confort.", + "proportional": "El término proporcional (kP) en el controlador PID, responsable de la respuesta inmediata a errores.", "sync_with_thermostat": "Sincronizar el punto de ajuste con el termostato para asegurar un control coordinado de la temperatura.", - "derivative_time_weight": "Un parámetro para ajustar la influencia del término derivativo a lo largo del tiempo, especialmente útil para reducir el infraimpulso durante la fase de calentamiento cuando el coeficiente de la curva de calefacción está correctamente ajustado." + "window_sensors": "Sensores de Contacto que activan el sistema cuando una ventana o puerta está abierta durante un período." + }, + "description": "Configuraciones y ajustes generales.", + "title": "General" + }, + "init": { + "menu_options": { + "advanced": "Opciones Avanzadas", + "general": "General", + "presets": "Preajustes", + "system_configuration": "Configuración del Sistema" } }, "presets": { - "title": "Preajustes", - "description": "Configuraciones de temperatura predefinidas para diferentes escenarios o actividades.", "data": { + "activity_temperature": "Temperatura de Actividad", "away_temperature": "Temperatura de Ausencia", + "comfort_temperature": "Temperatura de Confort", "home_temperature": "Temperatura de Casa", "sleep_temperature": "Temperatura de Sueño", - "comfort_temperature": "Temperatura de Confort", - "activity_temperature": "Temperatura de Actividad", "sync_climates_with_preset": "Sincronizar climas con preajuste (sueño / ausencia / actividad)" - } + }, + "description": "Configuraciones de temperatura predefinidas para diferentes escenarios o actividades.", + "title": "Preajustes" }, "system_configuration": { - "title": "Configuración del Sistema", - "description": "Para un ajuste fino y personalización.", "data": { "automatic_duty_cycle": "Ciclo de trabajo automático", "overshoot_protection": "Protección contra Sobrepasos (con PWM)", - "window_minimum_open_time": "Tiempo mínimo de apertura de ventana", - "sensor_max_value_age": "Edad máxima del valor del sensor de temperatura" + "sensor_max_value_age": "Edad máxima del valor del sensor de temperatura", + "window_minimum_open_time": "Tiempo mínimo de apertura de ventana" }, "data_description": { "automatic_duty_cycle": "Habilitar o deshabilitar el ciclo de trabajo automático para la Modulación de Ancho de Pulso (PWM).", "overshoot_protection": "Habilitar protección contra sobrepasos con Modulación de Ancho de Pulso (PWM) para prevenir excesos de temperatura de la caldera.", - "window_minimum_open_time": "El tiempo mínimo que una ventana debe estar abierta antes de que el sistema reaccione.", - "sensor_max_value_age": "La edad máxima del valor del sensor de temperatura antes de considerarlo como obsoleto." - } - }, - "advanced": { - "title": "Avanzadas", - "data": { - "simulation": "Simulación", - "sample_time": "Tiempo de Muestreo", - "thermal_comfort": "Confort Térmico", - "minimum_consumption": "Consumo Mínimo", - "maximum_consumption": "Consumo Máximo", - "climate_valve_offset": "Compensación de la Válvula Climática", - "target_temperature_step": "Paso de Temperatura Objetivo", - "maximum_relative_modulation": "Modulación Relativa Máxima", - "force_pulse_width_modulation": "Forzar Modulación de Ancho de Pulso", - "dynamic_minimum_setpoint": "Punto de Ajuste Mínimo Dinámico (Experimental)" + "sensor_max_value_age": "La edad máxima del valor del sensor de temperatura antes de considerarlo como obsoleto.", + "window_minimum_open_time": "El tiempo mínimo que una ventana debe estar abierta antes de que el sistema reaccione." }, - "data_description": { - "thermal_comfort": "Habilitar el uso del Índice de Simmer para ajuste de confort térmico.", - "minimum_consumption": "El consumo mínimo de gas cuando la caldera está activa.", - "maximum_consumption": "El consumo máximo de gas cuando la caldera está activa.", - "climate_valve_offset": "Compensación para ajustar el grado de apertura de la válvula climática.", - "target_temperature_step": "Ajustar el paso de la temperatura objetivo para una afinación precisa de los niveles de confort.", - "sample_time": "El intervalo de tiempo mínimo entre actualizaciones del controlador PID.", - "maximum_relative_modulation": "Representa el nivel más alto de modulación para un sistema de calefacción eficiente." - } + "description": "Para un ajuste fino y personalización.", + "title": "Configuración del Sistema" } } } diff --git a/custom_components/sat/translations/fr.json b/custom_components/sat/translations/fr.json index f0bc3513..011235d7 100644 --- a/custom_components/sat/translations/fr.json +++ b/custom_components/sat/translations/fr.json @@ -1,221 +1,225 @@ { "config": { + "abort": { + "already_configured": "La passerelle est déjà configurée." + }, + "error": { + "connection": "Impossible de se connecter à la passerelle.", + "mqtt_component": "Le composant MQTT n'est pas disponible.", + "unable_to_calibrate": "Le processus de calibration a rencontré un problème et n'a pas pu être complété avec succès. Veuillez vous assurer que votre système de chauffage fonctionne correctement et que tous les capteurs requis sont connectés et fonctionnent correctement.\n\nSi vous continuez à rencontrer des problèmes avec la calibration, envisagez de nous contacter pour obtenir de l'aide supplémentaire. Nous nous excusons pour tout désagrément causé." + }, + "progress": { + "calibration": "Calibration et recherche de la valeur de protection contre les dépassements en cours...\n\nVeuillez patienter pendant que nous optimisons votre système de chauffage. Ce processus peut prendre environ 20 minutes." + }, "step": { - "user": { - "title": "Smart Autotune Thermostat (SAT)", - "description": "Le SAT est un thermostat intelligent capable de s'auto-ajuster pour optimiser le contrôle de la température. Sélectionnez le mode approprié qui correspond à votre système de chauffage.", - "menu_options": { - "mosquitto": "OpenTherm Gateway ( MQTT )", - "serial": "OpenTherm Gateway ( SERIAL )", - "switch": "Thermostat PID avec PWM ( ON/OFF )", - "simulator": "Passerelle simulée ( AVANCÉ )" - } - }, - "mosquitto": { - "title": "OpenTherm Gateway ( MQTT )", - "description": "Veuillez fournir les détails suivants pour configurer la passerelle OpenTherm. Dans le champ Nom, entrez un nom pour la passerelle qui vous aidera à l'identifier au sein de votre système.\n\nSpécifiez l'entité Climat à utiliser pour la passerelle OpenTherm. Cette entité est fournie par la passerelle OpenTherm et représente votre système de chauffage.\n\nDe plus, entrez le Sujet principal qui sera utilisé pour publier et s'abonner aux messages MQTT liés à la passerelle OpenTherm.\n\nCes paramètres sont essentiels pour établir la communication et l'intégration avec votre passerelle OpenTherm via MQTT. Ils permettent un échange de données et un contrôle fluides de votre système de chauffage. Assurez-vous que les détails fournis sont précis pour garantir une fonctionnalité appropriée.", - "data": { - "name": "Nom", - "device": "Appareil", - "mqtt_topic": "Sujet Principal" - } - }, - "serial": { - "title": "OpenTherm Gateway ( SERIAL )", - "description": "Pour établir une connexion avec la passerelle OpenTherm en utilisant une connexion socket, veuillez fournir les détails suivants. Dans le champ Nom, entrez un nom pour la passerelle qui vous aidera à l'identifier au sein de votre système.\n\nSpécifiez l'adresse réseau de la passerelle OpenTherm dans le champ Appareil. Cela pourrait être au format \"socket://otgw.local:25238\", où \"otgw.local\" est le nom d'hôte ou l'adresse IP de la passerelle et \"25238\" est le numéro de port.\n\nCes paramètres sont essentiels pour établir la communication et l'intégration avec votre passerelle OpenTherm via la connexion socket. Assurez-vous que les détails fournis sont précis pour garantir une fonctionnalité appropriée.", - "data": { - "name": "Nom", - "device": "URL" - } - }, - "switch": { - "title": "Thermostat PID avec PWM (ON/OFF)", - "description": "Veuillez remplir les détails suivants pour configurer l'interrupteur. Entrez un nom pour l'interrupteur dans le champ Nom, ce qui vous aidera à l'identifier au sein de votre système. Choisissez l'entité appropriée à utiliser pour votre interrupteur parmi les options fournies.\n\nDans le champ Réglage de la température, spécifiez la température cible désirée pour votre système de chauffage. Si vous utilisez une chaudière à eau chaude, remplissez le Réglage de la température de la chaudière avec la valeur appropriée. Pour les systèmes de chauffage électrique, entrez la valeur 100.\n\nCes paramètres sont essentiels pour un contrôle précis de la température et pour garantir des performances optimales de votre système de chauffage. Fournir le Réglage de la température correct permet une régulation précise et contribue à créer un environnement confortable et économe en énergie dans votre maison.", - "data": { - "name": "Nom", - "device": "Entité", - "minimum_setpoint": "Réglage de la température" - } - }, - "simulator": { - "title": "Passerelle simulée ( AVANCÉ )", - "description": "Cette passerelle vous permet de simuler une chaudière à des fins de test et de démonstration. Veuillez fournir les informations suivantes pour configurer le simulateur.\n\nNote : La Passerelle Simulateur est destinée à des fins de test et de démonstration uniquement et ne doit pas être utilisée dans des environnements de production.", - "data": { - "name": "Nom", - "minimum_setpoint": "Réglage minimal", - "maximum_setpoint": "Réglage maximal", - "simulated_heating": "Chauffage simulé", - "simulated_cooling": "Refroidissement simulé", - "simulated_warming_up": "Réchauffement simulé" - } - }, - "sensors": { - "title": "Configurer les capteurs", - "description": "Veuillez sélectionner les capteurs qui seront utilisés pour suivre la température.", - "data": { - "inside_sensor_entity_id": "Entité du capteur intérieur", - "outside_sensor_entity_id": "Entité du capteur extérieur", - "humidity_sensor_entity_id": "Entité du capteur d'humidité" - } - }, - "heating_system": { - "title": "Système de chauffage", - "description": "Sélectionner le type de système de chauffage correct est important pour que le SAT contrôle précisément la température et optimise les performances. Choisissez l'option qui correspond à votre configuration pour garantir une régulation appropriée de la température dans votre maison.", - "data": { - "heating_system": "Système" - } - }, "areas": { - "title": "Zones", - "description": "Paramètres liés aux climats, aux pièces multiples et au contrôle de la température. Les climats principaux se trouvent dans la même pièce que le capteur intérieur et les pièces ont leurs propres températures cibles séparées du système.", "data": { "main_climates": "Principaux", "secondary_climates": "Pièces" - } + }, + "description": "Paramètres liés aux climats, aux pièces multiples et au contrôle de la température. Les climats principaux se trouvent dans la même pièce que le capteur intérieur et les pièces ont leurs propres températures cibles séparées du système.", + "title": "Zones" }, "automatic_gains": { - "title": "Gains automatiques", - "description": "Cette fonctionnalité ajuste les paramètres de contrôle de votre système de chauffage de manière dynamique, optimisant le contrôle de la température pour un meilleur confort et une meilleure efficacité énergétique. Activer cette option permet au SAT de s'adapter continuellement et d'affiner les réglages de chauffage en fonction des conditions environnementales. Cela aide à maintenir un environnement stable et confortable sans intervention manuelle.\n\nNote : Si vous choisissez de ne pas activer les gains automatiques, vous devrez entrer manuellement les valeurs PID pour un contrôle précis de la température. Veuillez vous assurer que vous disposez de valeurs PID précises pour votre système de chauffage spécifique afin d'obtenir des performances optimales.", "data": { "automatic_gains": "Gains automatiques (recommandé)" - } + }, + "description": "Cette fonctionnalité ajuste les paramètres de contrôle de votre système de chauffage de manière dynamique, optimisant le contrôle de la température pour un meilleur confort et une meilleure efficacité énergétique. Activer cette option permet au SAT de s'adapter continuellement et d'affiner les réglages de chauffage en fonction des conditions environnementales. Cela aide à maintenir un environnement stable et confortable sans intervention manuelle.\n\nNote : Si vous choisissez de ne pas activer les gains automatiques, vous devrez entrer manuellement les valeurs PID pour un contrôle précis de la température. Veuillez vous assurer que vous disposez de valeurs PID précises pour votre système de chauffage spécifique afin d'obtenir des performances optimales.", + "title": "Gains automatiques" }, "calibrate_system": { - "title": "Calibrer le système", "description": "Optimisez votre système de chauffage en déterminant automatiquement les valeurs PID optimales pour votre configuration. Lors de la sélection des Gains automatiques, veuillez noter que le système passera par un processus de calibration qui peut prendre environ 20 minutes à compléter.\n\nLes Gains automatiques sont recommandés pour la plupart des utilisateurs car ils simplifient le processus de configuration et garantissent des performances optimales. Cependant, si vous êtes familier avec le contrôle PID et que vous préférez définir manuellement les valeurs, vous pouvez choisir de ne pas activer les Gains automatiques.\n\nVeuillez noter que choisir de ne pas activer les Gains automatiques nécessite une bonne connaissance du contrôle PID et peut nécessiter des ajustements manuels supplémentaires pour obtenir des performances optimales.", "menu_options": { "calibrate": "Calibrer et déterminer votre valeur de protection contre les dépassements (env. 20 min).", "overshoot_protection": "Entrer manuellement la valeur de protection contre les dépassements.", "pid_controller": "Entrer manuellement les valeurs PID (non recommandé)." - } + }, + "title": "Calibrer le système" + }, + "calibrated": { + "description": "Le processus de calibration a été complété avec succès.\n\nFélicitations ! Votre Smart Autotune Thermostat (SAT) a été calibré pour optimiser la performance de chauffage de votre système. Au cours du processus de calibration, le SAT a analysé soigneusement les caractéristiques de chauffage et déterminé la valeur de protection contre les dépassements appropriée pour garantir un contrôle précis de la température.\n\nValeur de protection contre les dépassements : {minimum_setpoint} °C\n\nCette valeur représente la quantité maximale de dépassement autorisée pendant le processus de chauffage. Le SAT surveillera activement et ajustera le chauffage pour éviter un dépassement excessif, maintenant ainsi une expérience de chauffage confortable et efficace dans votre maison.\n\nVeuillez noter que la valeur de protection contre les dépassements peut varier en fonction des caractéristiques spécifiques de votre système de chauffage et des facteurs environnementaux. Elle a été affinée pour fournir des performances optimales basées sur les résultats de la calibration.", + "menu_options": { + "calibrate": "Réessayer la calibration", + "finish": "Continuer avec la calibration actuelle" + }, + "title": "Calibration terminée" + }, + "heating_system": { + "data": { + "heating_system": "Système" + }, + "description": "Sélectionner le type de système de chauffage correct est important pour que le SAT contrôle précisément la température et optimise les performances. Choisissez l'option qui correspond à votre configuration pour garantir une régulation appropriée de la température dans votre maison.", + "title": "Système de chauffage" + }, + "mosquitto": { + "data": { + "device": "Appareil", + "mqtt_topic": "Sujet Principal", + "name": "Nom" + }, + "description": "Veuillez fournir les détails suivants pour configurer la passerelle OpenTherm. Dans le champ Nom, entrez un nom pour la passerelle qui vous aidera à l'identifier au sein de votre système.\n\nSpécifiez l'entité Climat à utiliser pour la passerelle OpenTherm. Cette entité est fournie par la passerelle OpenTherm et représente votre système de chauffage.\n\nDe plus, entrez le Sujet principal qui sera utilisé pour publier et s'abonner aux messages MQTT liés à la passerelle OpenTherm.\n\nCes paramètres sont essentiels pour établir la communication et l'intégration avec votre passerelle OpenTherm via MQTT. Ils permettent un échange de données et un contrôle fluides de votre système de chauffage. Assurez-vous que les détails fournis sont précis pour garantir une fonctionnalité appropriée.", + "title": "OpenTherm Gateway ( MQTT )" }, "overshoot_protection": { - "title": "Protection contre les dépassements", - "description": "En fournissant la valeur de protection contre les dépassements, le SAT ajustera les paramètres de contrôle en conséquence pour maintenir un environnement de chauffage stable et confortable. Cette configuration manuelle vous permet d'affiner le système en fonction de votre configuration spécifique.\n\nNote : Si vous n'êtes pas sûr de la valeur de protection contre les dépassements ou si vous n'avez pas effectué le processus de calibration, il est recommandé d'annuler la configuration et de passer par le processus de calibration pour permettre au SAT de déterminer automatiquement la valeur pour des performances optimales.", "data": { "minimum_setpoint": "Valeur" - } + }, + "description": "En fournissant la valeur de protection contre les dépassements, le SAT ajustera les paramètres de contrôle en conséquence pour maintenir un environnement de chauffage stable et confortable. Cette configuration manuelle vous permet d'affiner le système en fonction de votre configuration spécifique.\n\nNote : Si vous n'êtes pas sûr de la valeur de protection contre les dépassements ou si vous n'avez pas effectué le processus de calibration, il est recommandé d'annuler la configuration et de passer par le processus de calibration pour permettre au SAT de déterminer automatiquement la valeur pour des performances optimales.", + "title": "Protection contre les dépassements" }, "pid_controller": { - "title": "Configurer manuellement le contrôleur PID.", - "description": "Configurez manuellement les gains proportionnel, intégral et dérivé pour affiner votre système de chauffage. Utilisez cette option si vous préférez avoir un contrôle total sur les paramètres du contrôleur PID. Ajustez les gains en fonction des caractéristiques spécifiques de votre système de chauffage et de vos préférences.", "data": { - "integral": "Intégral (kI)", "derivative": "Dérivé (kD)", + "integral": "Intégral (kI)", "proportional": "Proportionnel (kP)" - } + }, + "description": "Configurez manuellement les gains proportionnel, intégral et dérivé pour affiner votre système de chauffage. Utilisez cette option si vous préférez avoir un contrôle total sur les paramètres du contrôleur PID. Ajustez les gains en fonction des caractéristiques spécifiques de votre système de chauffage et de vos préférences.", + "title": "Configurer manuellement le contrôleur PID." }, - "calibrated": { - "title": "Calibration terminée", - "description": "Le processus de calibration a été complété avec succès.\n\nFélicitations ! Votre Smart Autotune Thermostat (SAT) a été calibré pour optimiser la performance de chauffage de votre système. Au cours du processus de calibration, le SAT a analysé soigneusement les caractéristiques de chauffage et déterminé la valeur de protection contre les dépassements appropriée pour garantir un contrôle précis de la température.\n\nValeur de protection contre les dépassements : {minimum_setpoint} °C\n\nCette valeur représente la quantité maximale de dépassement autorisée pendant le processus de chauffage. Le SAT surveillera activement et ajustera le chauffage pour éviter un dépassement excessif, maintenant ainsi une expérience de chauffage confortable et efficace dans votre maison.\n\nVeuillez noter que la valeur de protection contre les dépassements peut varier en fonction des caractéristiques spécifiques de votre système de chauffage et des facteurs environnementaux. Elle a été affinée pour fournir des performances optimales basées sur les résultats de la calibration.", + "sensors": { + "data": { + "humidity_sensor_entity_id": "Entité du capteur d'humidité", + "inside_sensor_entity_id": "Entité du capteur intérieur", + "outside_sensor_entity_id": "Entité du capteur extérieur" + }, + "description": "Veuillez sélectionner les capteurs qui seront utilisés pour suivre la température.", + "title": "Configurer les capteurs" + }, + "serial": { + "data": { + "device": "URL", + "name": "Nom" + }, + "description": "Pour établir une connexion avec la passerelle OpenTherm en utilisant une connexion socket, veuillez fournir les détails suivants. Dans le champ Nom, entrez un nom pour la passerelle qui vous aidera à l'identifier au sein de votre système.\n\nSpécifiez l'adresse réseau de la passerelle OpenTherm dans le champ Appareil. Cela pourrait être au format \"socket://otgw.local:25238\", où \"otgw.local\" est le nom d'hôte ou l'adresse IP de la passerelle et \"25238\" est le numéro de port.\n\nCes paramètres sont essentiels pour établir la communication et l'intégration avec votre passerelle OpenTherm via la connexion socket. Assurez-vous que les détails fournis sont précis pour garantir une fonctionnalité appropriée.", + "title": "OpenTherm Gateway ( SERIAL )" + }, + "simulator": { + "data": { + "maximum_setpoint": "Réglage maximal", + "minimum_setpoint": "Réglage minimal", + "name": "Nom", + "simulated_cooling": "Refroidissement simulé", + "simulated_heating": "Chauffage simulé", + "simulated_warming_up": "Réchauffement simulé" + }, + "description": "Cette passerelle vous permet de simuler une chaudière à des fins de test et de démonstration. Veuillez fournir les informations suivantes pour configurer le simulateur.\n\nNote : La Passerelle Simulateur est destinée à des fins de test et de démonstration uniquement et ne doit pas être utilisée dans des environnements de production.", + "title": "Passerelle simulée ( AVANCÉ )" + }, + "switch": { + "data": { + "device": "Entité", + "minimum_setpoint": "Réglage de la température", + "name": "Nom" + }, + "description": "Veuillez remplir les détails suivants pour configurer l'interrupteur. Entrez un nom pour l'interrupteur dans le champ Nom, ce qui vous aidera à l'identifier au sein de votre système. Choisissez l'entité appropriée à utiliser pour votre interrupteur parmi les options fournies.\n\nDans le champ Réglage de la température, spécifiez la température cible désirée pour votre système de chauffage. Si vous utilisez une chaudière à eau chaude, remplissez le Réglage de la température de la chaudière avec la valeur appropriée. Pour les systèmes de chauffage électrique, entrez la valeur 100.\n\nCes paramètres sont essentiels pour un contrôle précis de la température et pour garantir des performances optimales de votre système de chauffage. Fournir le Réglage de la température correct permet une régulation précise et contribue à créer un environnement confortable et économe en énergie dans votre maison.", + "title": "Thermostat PID avec PWM (ON/OFF)" + }, + "user": { + "description": "Le SAT est un thermostat intelligent capable de s'auto-ajuster pour optimiser le contrôle de la température. Sélectionnez le mode approprié qui correspond à votre système de chauffage.", "menu_options": { - "calibrate": "Réessayer la calibration", - "finish": "Continuer avec la calibration actuelle" - } + "mosquitto": "OpenTherm Gateway ( MQTT )", + "serial": "OpenTherm Gateway ( SERIAL )", + "simulator": "Passerelle simulée ( AVANCÉ )", + "switch": "Thermostat PID avec PWM ( ON/OFF )" + }, + "title": "Smart Autotune Thermostat (SAT)" } - }, - "error": { - "connection": "Impossible de se connecter à la passerelle.", - "mqtt_component": "Le composant MQTT n'est pas disponible.", - "unable_to_calibrate": "Le processus de calibration a rencontré un problème et n'a pas pu être complété avec succès. Veuillez vous assurer que votre système de chauffage fonctionne correctement et que tous les capteurs requis sont connectés et fonctionnent correctement.\n\nSi vous continuez à rencontrer des problèmes avec la calibration, envisagez de nous contacter pour obtenir de l'aide supplémentaire. Nous nous excusons pour tout désagrément causé." - }, - "abort": { - "already_configured": "La passerelle est déjà configurée." - }, - "progress": { - "calibration": "Calibration et recherche de la valeur de protection contre les dépassements en cours...\n\nVeuillez patienter pendant que nous optimisons votre système de chauffage. Ce processus peut prendre environ 20 minutes." } }, "options": { "step": { - "init": { - "menu_options": { - "general": "Général", - "presets": "Préréglages", - "advanced": "Options Avancées", - "system_configuration": "Configuration du Système" - } + "advanced": { + "data": { + "climate_valve_offset": "Décalage de la vanne climatique", + "dynamic_minimum_setpoint": "Point de Consigne Minimum Dynamique (Expérimental)", + "force_pulse_width_modulation": "Forcer la Modulation de Largeur d'Impulsion", + "maximum_consumption": "Consommation Maximale", + "maximum_relative_modulation": "Modulation Relative Maximale", + "minimum_consumption": "Consommation Minimale", + "sample_time": "Temps d'Échantillonnage", + "simulation": "Simulation", + "target_temperature_step": "Pas de Température Cible", + "thermal_comfort": "Confort Thermique" + }, + "data_description": { + "climate_valve_offset": "Décalage pour ajuster le degré d'ouverture de la vanne climatique.", + "dynamic_minimum_setpoint": "Active l'ajustement dynamique du point de consigne minimal en fonction de la température de retour de la chaudière, ce qui aide également à identifier si des vannes sont fermées.", + "maximum_consumption": "La consommation maximale de gaz lorsque la chaudière est active.", + "maximum_relative_modulation": "Représentant le niveau de modulation le plus élevé pour un système de chauffage efficace.", + "minimum_consumption": "La consommation minimale de gaz lorsque la chaudière est active.", + "sample_time": "L'intervalle de temps minimum entre les mises à jour du régulateur PID.", + "target_temperature_step": "Ajuster le pas de température cible pour un réglage fin des niveaux de confort.", + "thermal_comfort": "Activer l'utilisation de l'Indice de Simmer pour ajuster le confort thermique." + }, + "title": "Avancé" }, "general": { - "title": "Général", - "description": "Paramètres et configurations généraux.", "data": { - "integral": "Intégral (kI)", - "derivative": "Dérivé (kD)", - "proportional": "Proportionnel (kP)", - "maximum_setpoint": "Point de consigne maximal", - "window_sensors": "Capteurs de Contact", "automatic_gains_value": "Valeur de Gains Automatiques", + "derivative": "Dérivé (kD)", "derivative_time_weight": "Poids Temporel Dérivé", - "heating_curve_version": "Version de la Courbe de Chauffage", - "heating_curve_coefficient": "Coefficient de la Courbe de Chauffage", "duty_cycle": "Cycle de Fonctionnement Maximum pour la Modulation de Largeur d'Impulsion", - "sync_with_thermostat": "Synchroniser le point de consigne avec le thermostat" + "heating_curve_coefficient": "Coefficient de la Courbe de Chauffage", + "heating_curve_version": "Version de la Courbe de Chauffage", + "integral": "Intégral (kI)", + "maximum_setpoint": "Point de consigne maximal", + "minimum_setpoint_adjustment_factor": "Facteur d'ajustement du point de consigne minimal", + "pid_controller_version": "Version du contrôleur PID", + "proportional": "Proportionnel (kP)", + "sync_with_thermostat": "Synchroniser le point de consigne avec le thermostat", + "window_sensors": "Capteurs de Contact" }, "data_description": { - "integral": "Le terme intégral (kI) dans le régulateur PID, responsable de la réduction de l'erreur en régime permanent.", - "derivative": "Le terme dérivé (kD) dans le régulateur PID, responsable de l'atténuation des dépassements.", - "proportional": "Le terme proportionnel (kP) dans le régulateur PID, responsable de la réponse immédiate aux erreurs.", - "maximum_setpoint": "La température optimale pour un fonctionnement efficace de la chaudière.", - "window_sensors": "Capteurs de Contact qui déclenchent le système lorsqu'une fenêtre ou une porte est ouverte pendant une période.", "automatic_gains_value": "La valeur utilisée pour les gains automatiques dans le régulateur PID.", - "heating_curve_coefficient": "Le coefficient utilisé pour ajuster la courbe de chauffage.", + "derivative": "Le terme dérivé (kD) dans le régulateur PID, responsable de l'atténuation des dépassements.", + "derivative_time_weight": "Un paramètre pour ajuster l'influence du terme dérivé au fil du temps, particulièrement utile pour réduire le dépassement lors de la phase de montée en température lorsque le coefficient de la courbe de chauffage est correctement réglé.", "duty_cycle": "Le cycle de fonctionnement maximum pour la Modulation de Largeur d'Impulsion (PWM), contrôlant les cycles de marche/arrêt de la chaudière.", + "heating_curve_coefficient": "Le coefficient utilisé pour ajuster la courbe de chauffage.", + "integral": "Le terme intégral (kI) dans le régulateur PID, responsable de la réduction de l'erreur en régime permanent.", + "maximum_setpoint": "La température optimale pour un fonctionnement efficace de la chaudière.", + "minimum_setpoint_adjustment_factor": "Ce facteur ajuste le point de consigne du chauffage en fonction de la température de retour de la chaudière, influençant la réactivité et l'efficacité du chauffage. Une valeur plus élevée augmente la sensibilité aux changements de température, améliorant le contrôle du confort et de l'utilisation de l'énergie. La plage de départ recommandée est de 0,1 à 0,5. Ajustez pour convenir à votre système et à vos préférences de confort.", + "proportional": "Le terme proportionnel (kP) dans le régulateur PID, responsable de la réponse immédiate aux erreurs.", "sync_with_thermostat": "Synchroniser le point de consigne avec le thermostat pour assurer une régulation coordonnée de la température.", - "derivative_time_weight": "Un paramètre pour ajuster l'influence du terme dérivé au fil du temps, particulièrement utile pour réduire le dépassement lors de la phase de montée en température lorsque le coefficient de la courbe de chauffage est correctement réglé." + "window_sensors": "Capteurs de Contact qui déclenchent le système lorsqu'une fenêtre ou une porte est ouverte pendant une période." + }, + "description": "Paramètres et configurations généraux.", + "title": "Général" + }, + "init": { + "menu_options": { + "advanced": "Options Avancées", + "general": "Général", + "presets": "Préréglages", + "system_configuration": "Configuration du Système" } }, "presets": { - "title": "Préréglages", - "description": "Paramètres de température prédéfinis pour différents scénarios ou activités.", "data": { + "activity_temperature": "Température Activité", "away_temperature": "Température Absence", + "comfort_temperature": "Température Confort", "home_temperature": "Température Maison", "sleep_temperature": "Température Sommeil", - "comfort_temperature": "Température Confort", - "activity_temperature": "Température Activité", "sync_climates_with_preset": "Synchroniser les climats avec le préréglage (sommeil / absence / activité)" - } + }, + "description": "Paramètres de température prédéfinis pour différents scénarios ou activités.", + "title": "Préréglages" }, "system_configuration": { - "title": "Configuration du Système", - "description": "Pour un réglage fin et une personnalisation.", "data": { "automatic_duty_cycle": "Cycle de fonctionnement automatique", "overshoot_protection": "Protection contre le dépassement (avec PWM)", - "window_minimum_open_time": "Temps minimum d'ouverture de la fenêtre", - "sensor_max_value_age": "Âge maximal de la valeur du capteur de température" + "sensor_max_value_age": "Âge maximal de la valeur du capteur de température", + "window_minimum_open_time": "Temps minimum d'ouverture de la fenêtre" }, "data_description": { "automatic_duty_cycle": "Activer ou désactiver le cycle de fonctionnement automatique pour la Modulation de Largeur d'Impulsion (PWM).", "overshoot_protection": "Activer la protection contre le dépassement avec la Modulation de Largeur d'Impulsion (PWM) pour prévenir les dépassements de température de la chaudière.", - "window_minimum_open_time": "Le temps minimum qu'une fenêtre doit être ouverte avant que le système ne réagisse.", - "sensor_max_value_age": "L'âge maximum de la valeur du capteur de température avant de la considérer comme stagnante." - } - }, - "advanced": { - "title": "Avancé", - "data": { - "simulation": "Simulation", - "sample_time": "Temps d'Échantillonnage", - "thermal_comfort": "Confort Thermique", - "minimum_consumption": "Consommation Minimale", - "maximum_consumption": "Consommation Maximale", - "climate_valve_offset": "Décalage de la vanne climatique", - "target_temperature_step": "Pas de Température Cible", - "maximum_relative_modulation": "Modulation Relative Maximale", - "force_pulse_width_modulation": "Forcer la Modulation de Largeur d'Impulsion", - "dynamic_minimum_setpoint": "Point de Consigne Minimum Dynamique (Expérimental)" + "sensor_max_value_age": "L'âge maximum de la valeur du capteur de température avant de la considérer comme stagnante.", + "window_minimum_open_time": "Le temps minimum qu'une fenêtre doit être ouverte avant que le système ne réagisse." }, - "data_description": { - "thermal_comfort": "Activer l'utilisation de l'Indice de Simmer pour ajuster le confort thermique.", - "minimum_consumption": "La consommation minimale de gaz lorsque la chaudière est active.", - "maximum_consumption": "La consommation maximale de gaz lorsque la chaudière est active.", - "climate_valve_offset": "Décalage pour ajuster le degré d'ouverture de la vanne climatique.", - "target_temperature_step": "Ajuster le pas de température cible pour un réglage fin des niveaux de confort.", - "sample_time": "L'intervalle de temps minimum entre les mises à jour du régulateur PID.", - "maximum_relative_modulation": "Représentant le niveau de modulation le plus élevé pour un système de chauffage efficace." - } + "description": "Pour un réglage fin et une personnalisation.", + "title": "Configuration du Système" } } } diff --git a/custom_components/sat/translations/it.json b/custom_components/sat/translations/it.json index 2a63f1d0..3f51f53f 100644 --- a/custom_components/sat/translations/it.json +++ b/custom_components/sat/translations/it.json @@ -1,221 +1,225 @@ { "config": { + "abort": { + "already_configured": "Il gateway è già configurato." + }, + "error": { + "connection": "Impossibile connettersi al gateway.", + "mqtt_component": "Il componente MQTT non è disponibile.", + "unable_to_calibrate": "Il processo di calibrazione ha incontrato un problema e non è stato completato con successo. Si prega di assicurarsi che il sistema di riscaldamento funzioni correttamente e che tutti i sensori richiesti siano connessi e funzionanti correttamente.\n\nSe continui a riscontrare problemi con la calibrazione, considera di contattarci per ulteriore assistenza. Ci scusiamo per eventuali inconvenienti causati." + }, + "progress": { + "calibration": "Calibrazione e ricerca del valore di protezione dal superamento in corso...\n\nSi prega di attendere mentre ottimizziamo il tuo sistema di riscaldamento. Questo processo può richiedere circa 20 minuti." + }, "step": { - "user": { - "title": "Smart Autotune Thermostat (SAT)", - "description": "Il SAT è un termostato intelligente capace di auto-regolarsi per ottimizzare il controllo della temperatura. Seleziona la modalità appropriata che corrisponde al tuo sistema di riscaldamento.", - "menu_options": { - "mosquitto": "Gateway OpenTherm (MQTT)", - "serial": "Gateway OpenTherm (SERIALE)", - "switch": "Termostato PID con PWM (ON/OFF)", - "simulator": "Gateway Simulato (AVANZATO)" - } - }, - "mosquitto": { - "title": "Gateway OpenTherm (MQTT)", - "description": "Si prega di fornire i seguenti dettagli per configurare il Gateway OpenTherm. Nel campo Nome, inserisci un nome per il gateway che ti aiuterà a identificarlo all'interno del tuo sistema.\n\nSpecifica l'entità Climatica da utilizzare per il Gateway OpenTherm. Questa entità è fornita dal Gateway OpenTherm e rappresenta il tuo sistema di riscaldamento.\n\nInoltre, inserisci l'Argomento Principale che verrà utilizzato per pubblicare e sottoscrivere i messaggi MQTT relativi al Gateway OpenTherm.\n\nQueste impostazioni sono essenziali per stabilire la comunicazione e l'integrazione con il tuo Gateway OpenTherm tramite MQTT. Consentono uno scambio di dati e un controllo fluido del tuo sistema di riscaldamento. Assicurati che i dettagli forniti siano accurati per garantire un corretto funzionamento.", - "data": { - "name": "Nome", - "device": "Dispositivo", - "mqtt_topic": "Argomento Principale" - } - }, - "serial": { - "title": "Gateway OpenTherm (SERIALE)", - "description": "Per stabilire una connessione con il Gateway OpenTherm tramite una connessione socket, si prega di fornire i seguenti dettagli. Nel campo Nome, inserisci un nome per il gateway che ti aiuterà a identificarlo all'interno del tuo sistema.\n\nSpecifica l'indirizzo di rete del Gateway OpenTherm nel campo Dispositivo. Questo potrebbe essere nel formato \"socket://otgw.local:25238\", dove \"otgw.local\" è il nome host o l'indirizzo IP del gateway e \"25238\" è il numero della porta.\n\nQueste impostazioni sono essenziali per stabilire la comunicazione e l'integrazione con il tuo Gateway OpenTherm tramite la connessione socket. Assicurati che i dettagli forniti siano accurati per garantire un corretto funzionamento.", - "data": { - "name": "Nome", - "device": "URL" - } - }, - "switch": { - "title": "Termostato PID con PWM (ON/OFF)", - "description": "Si prega di compilare i seguenti dettagli per configurare l'interruttore. Inserisci un nome per l'interruttore nel campo Nome, che ti aiuterà a identificarlo all'interno del tuo sistema. Scegli l'entità appropriata da utilizzare per il tuo interruttore tra le opzioni fornite.\n\nNel campo Impostazione della Temperatura, specifica la temperatura desiderata per il tuo sistema di riscaldamento. Se stai utilizzando una caldaia per acqua calda, inserisci l'Impostazione della Temperatura della Caldaia con il valore appropriato. Per i sistemi di riscaldamento elettrici, inserisci il valore 100.\n\nQueste impostazioni sono essenziali per un controllo preciso della temperatura e per garantire prestazioni ottimali del tuo sistema di riscaldamento. Fornire l'Impostazione della Temperatura corretta consente una regolazione accurata e contribuisce a creare un ambiente confortevole ed efficiente dal punto di vista energetico nella tua casa.", - "data": { - "name": "Nome", - "device": "Entità", - "minimum_setpoint": "Impostazione della Temperatura" - } - }, - "simulator": { - "title": "Gateway Simulato (AVANZATO)", - "description": "Questo gateway ti consente di simulare una caldaia per scopi di test e dimostrazione. Si prega di fornire le seguenti informazioni per configurare il simulatore.\n\nNota: Il Gateway Simulatore è destinato solo a scopi di test e dimostrazione e non dovrebbe essere utilizzato in ambienti di produzione.", - "data": { - "name": "Nome", - "minimum_setpoint": "Setpoint Minimo", - "maximum_setpoint": "Setpoint Massimo", - "simulated_heating": "Riscaldamento Simulato", - "simulated_cooling": "Raffreddamento Simulato", - "simulated_warming_up": "Riscaldamento Simulato" - } - }, - "sensors": { - "title": "Configura sensori", - "description": "Si prega di selezionare i sensori che verranno utilizzati per monitorare la temperatura.", - "data": { - "inside_sensor_entity_id": "Entità Sensore Interno", - "outside_sensor_entity_id": "Entità Sensore Esterno", - "humidity_sensor_entity_id": "Entità Sensore Umidità" - } - }, - "heating_system": { - "title": "Sistema di Riscaldamento", - "description": "Selezionare il tipo corretto di sistema di riscaldamento è importante affinché il SAT possa controllare accuratamente la temperatura e ottimizzare le prestazioni. Scegli l'opzione che corrisponde alla tua configurazione per garantire una regolazione appropriata della temperatura in tutta la tua casa.", - "data": { - "heating_system": "Sistema" - } - }, "areas": { - "title": "Aree", - "description": "Impostazioni relative ai climi, alle stanze multiple e al controllo della temperatura. I climi primari sono nella stessa stanza del sensore interno e le stanze hanno le loro temperature obiettivo separate dal sistema.", "data": { "main_climates": "Primari", "secondary_climates": "Stanze" - } + }, + "description": "Impostazioni relative ai climi, alle stanze multiple e al controllo della temperatura. I climi primari sono nella stessa stanza del sensore interno e le stanze hanno le loro temperature obiettivo separate dal sistema.", + "title": "Aree" }, "automatic_gains": { - "title": "Guadagni Automatici", - "description": "Questa funzionalità regola dinamicamente i parametri di controllo del tuo sistema di riscaldamento, ottimizzando il controllo della temperatura per un maggiore comfort e efficienza energetica. Attivare questa opzione consente al SAT di adattarsi continuamente e di perfezionare le impostazioni di riscaldamento in base alle condizioni ambientali. Ciò aiuta a mantenere un ambiente stabile e confortevole senza interventi manuali.\n\nNota: Se scegli di non abilitare i guadagni automatici, dovrai inserire manualmente i valori PID per un controllo preciso della temperatura. Assicurati di avere valori PID accurati per il tuo specifico sistema di riscaldamento per ottenere prestazioni ottimali.", "data": { "automatic_gains": "Guadagni Automatici (raccomandato)" - } + }, + "description": "Questa funzionalità regola dinamicamente i parametri di controllo del tuo sistema di riscaldamento, ottimizzando il controllo della temperatura per un maggiore comfort e efficienza energetica. Attivare questa opzione consente al SAT di adattarsi continuamente e di perfezionare le impostazioni di riscaldamento in base alle condizioni ambientali. Ciò aiuta a mantenere un ambiente stabile e confortevole senza interventi manuali.\n\nNota: Se scegli di non abilitare i guadagni automatici, dovrai inserire manualmente i valori PID per un controllo preciso della temperatura. Assicurati di avere valori PID accurati per il tuo specifico sistema di riscaldamento per ottenere prestazioni ottimali.", + "title": "Guadagni Automatici" }, "calibrate_system": { - "title": "Calibra Sistema", "description": "Ottimizza il tuo sistema di riscaldamento determinando automaticamente i valori PID ottimali per la tua configurazione. Quando selezioni Guadagni Automatici, si prega di notare che il sistema passerà attraverso un processo di calibrazione che potrebbe richiedere circa 20 minuti per completarsi.\n\nI Guadagni Automatici sono raccomandati per la maggior parte degli utenti in quanto semplificano il processo di configurazione e garantiscono prestazioni ottimali. Tuttavia, se sei familiare con il controllo PID e preferisci impostare manualmente i valori, puoi scegliere di non attivare i Guadagni Automatici.\n\nSi prega di notare che scegliere di non attivare i Guadagni Automatici richiede una buona conoscenza del controllo PID e potrebbe richiedere ulteriori regolazioni manuali per ottenere prestazioni ottimali.", "menu_options": { "calibrate": "Calibra e determina il tuo valore di protezione dal superamento (circa 20 min).", "overshoot_protection": "Inserisci manualmente il valore di protezione dal superamento.", "pid_controller": "Inserisci manualmente i valori PID (non raccomandato)." - } + }, + "title": "Calibra Sistema" + }, + "calibrated": { + "description": "Il processo di calibrazione è stato completato con successo.\n\nCongratulazioni! Il tuo Smart Autotune Thermostat (SAT) è stato calibrato per ottimizzare le prestazioni di riscaldamento del tuo sistema. Durante il processo di calibrazione, il SAT ha analizzato attentamente le caratteristiche di riscaldamento e determinato il valore appropriato di protezione dal superamento per garantire un controllo preciso della temperatura.\n\nValore di Protezione dal Superamento: {minimum_setpoint} °C\n\nQuesto valore rappresenta la quantità massima di superamento consentita durante il processo di riscaldamento. Il SAT monitorerà attivamente e regolerà il riscaldamento per evitare un superamento eccessivo, mantenendo così un'esperienza di riscaldamento confortevole ed efficiente nella tua casa.\n\nSi prega di notare che il valore di protezione dal superamento può variare a seconda delle caratteristiche specifiche del tuo sistema di riscaldamento e dei fattori ambientali. È stato affinato per fornire prestazioni ottimali in base ai risultati della calibrazione.", + "menu_options": { + "calibrate": "Riprova la calibrazione", + "finish": "Continua con la calibrazione attuale" + }, + "title": "Calibrazione Completata" + }, + "heating_system": { + "data": { + "heating_system": "Sistema" + }, + "description": "Selezionare il tipo corretto di sistema di riscaldamento è importante affinché il SAT possa controllare accuratamente la temperatura e ottimizzare le prestazioni. Scegli l'opzione che corrisponde alla tua configurazione per garantire una regolazione appropriata della temperatura in tutta la tua casa.", + "title": "Sistema di Riscaldamento" + }, + "mosquitto": { + "data": { + "device": "Dispositivo", + "mqtt_topic": "Argomento Principale", + "name": "Nome" + }, + "description": "Si prega di fornire i seguenti dettagli per configurare il Gateway OpenTherm. Nel campo Nome, inserisci un nome per il gateway che ti aiuterà a identificarlo all'interno del tuo sistema.\n\nSpecifica l'entità Climatica da utilizzare per il Gateway OpenTherm. Questa entità è fornita dal Gateway OpenTherm e rappresenta il tuo sistema di riscaldamento.\n\nInoltre, inserisci l'Argomento Principale che verrà utilizzato per pubblicare e sottoscrivere i messaggi MQTT relativi al Gateway OpenTherm.\n\nQueste impostazioni sono essenziali per stabilire la comunicazione e l'integrazione con il tuo Gateway OpenTherm tramite MQTT. Consentono uno scambio di dati e un controllo fluido del tuo sistema di riscaldamento. Assicurati che i dettagli forniti siano accurati per garantire un corretto funzionamento.", + "title": "Gateway OpenTherm (MQTT)" }, "overshoot_protection": { - "title": "Protezione dal Superamento", - "description": "Fornendo il valore di protezione dal superamento, il SAT regolerà di conseguenza i parametri di controllo per mantenere un ambiente di riscaldamento stabile e confortevole. Questa configurazione manuale ti consente di affinare il sistema in base alla tua configurazione specifica.\n\nNota: Se non sei sicuro del valore di protezione dal superamento o non hai eseguito il processo di calibrazione, si raccomanda di annullare la configurazione e procedere con il processo di calibrazione per consentire al SAT di determinare automaticamente il valore per prestazioni ottimali.", "data": { "minimum_setpoint": "Valore" - } + }, + "description": "Fornendo il valore di protezione dal superamento, il SAT regolerà di conseguenza i parametri di controllo per mantenere un ambiente di riscaldamento stabile e confortevole. Questa configurazione manuale ti consente di affinare il sistema in base alla tua configurazione specifica.\n\nNota: Se non sei sicuro del valore di protezione dal superamento o non hai eseguito il processo di calibrazione, si raccomanda di annullare la configurazione e procedere con il processo di calibrazione per consentire al SAT di determinare automaticamente il valore per prestazioni ottimali.", + "title": "Protezione dal Superamento" }, "pid_controller": { - "title": "Configura manualmente il controller PID.", - "description": "Configura manualmente i guadagni proporzionale, integrale e derivativo per affinare il tuo sistema di riscaldamento. Utilizza questa opzione se preferisci avere il pieno controllo sui parametri del controller PID. Regola i guadagni in base alle caratteristiche specifiche del tuo sistema di riscaldamento e alle tue preferenze.", "data": { - "integral": "Integrale (kI)", "derivative": "Derivativo (kD)", + "integral": "Integrale (kI)", "proportional": "Proporzionale (kP)" - } + }, + "description": "Configura manualmente i guadagni proporzionale, integrale e derivativo per affinare il tuo sistema di riscaldamento. Utilizza questa opzione se preferisci avere il pieno controllo sui parametri del controller PID. Regola i guadagni in base alle caratteristiche specifiche del tuo sistema di riscaldamento e alle tue preferenze.", + "title": "Configura manualmente il controller PID." }, - "calibrated": { - "title": "Calibrazione Completata", - "description": "Il processo di calibrazione è stato completato con successo.\n\nCongratulazioni! Il tuo Smart Autotune Thermostat (SAT) è stato calibrato per ottimizzare le prestazioni di riscaldamento del tuo sistema. Durante il processo di calibrazione, il SAT ha analizzato attentamente le caratteristiche di riscaldamento e determinato il valore appropriato di protezione dal superamento per garantire un controllo preciso della temperatura.\n\nValore di Protezione dal Superamento: {minimum_setpoint} °C\n\nQuesto valore rappresenta la quantità massima di superamento consentita durante il processo di riscaldamento. Il SAT monitorerà attivamente e regolerà il riscaldamento per evitare un superamento eccessivo, mantenendo così un'esperienza di riscaldamento confortevole ed efficiente nella tua casa.\n\nSi prega di notare che il valore di protezione dal superamento può variare a seconda delle caratteristiche specifiche del tuo sistema di riscaldamento e dei fattori ambientali. È stato affinato per fornire prestazioni ottimali in base ai risultati della calibrazione.", + "sensors": { + "data": { + "humidity_sensor_entity_id": "Entità Sensore Umidità", + "inside_sensor_entity_id": "Entità Sensore Interno", + "outside_sensor_entity_id": "Entità Sensore Esterno" + }, + "description": "Si prega di selezionare i sensori che verranno utilizzati per monitorare la temperatura.", + "title": "Configura sensori" + }, + "serial": { + "data": { + "device": "URL", + "name": "Nome" + }, + "description": "Per stabilire una connessione con il Gateway OpenTherm tramite una connessione socket, si prega di fornire i seguenti dettagli. Nel campo Nome, inserisci un nome per il gateway che ti aiuterà a identificarlo all'interno del tuo sistema.\n\nSpecifica l'indirizzo di rete del Gateway OpenTherm nel campo Dispositivo. Questo potrebbe essere nel formato \"socket://otgw.local:25238\", dove \"otgw.local\" è il nome host o l'indirizzo IP del gateway e \"25238\" è il numero della porta.\n\nQueste impostazioni sono essenziali per stabilire la comunicazione e l'integrazione con il tuo Gateway OpenTherm tramite la connessione socket. Assicurati che i dettagli forniti siano accurati per garantire un corretto funzionamento.", + "title": "Gateway OpenTherm (SERIALE)" + }, + "simulator": { + "data": { + "maximum_setpoint": "Setpoint Massimo", + "minimum_setpoint": "Setpoint Minimo", + "name": "Nome", + "simulated_cooling": "Raffreddamento Simulato", + "simulated_heating": "Riscaldamento Simulato", + "simulated_warming_up": "Riscaldamento Simulato" + }, + "description": "Questo gateway ti consente di simulare una caldaia per scopi di test e dimostrazione. Si prega di fornire le seguenti informazioni per configurare il simulatore.\n\nNota: Il Gateway Simulatore è destinato solo a scopi di test e dimostrazione e non dovrebbe essere utilizzato in ambienti di produzione.", + "title": "Gateway Simulato (AVANZATO)" + }, + "switch": { + "data": { + "device": "Entità", + "minimum_setpoint": "Impostazione della Temperatura", + "name": "Nome" + }, + "description": "Si prega di compilare i seguenti dettagli per configurare l'interruttore. Inserisci un nome per l'interruttore nel campo Nome, che ti aiuterà a identificarlo all'interno del tuo sistema. Scegli l'entità appropriata da utilizzare per il tuo interruttore tra le opzioni fornite.\n\nNel campo Impostazione della Temperatura, specifica la temperatura desiderata per il tuo sistema di riscaldamento. Se stai utilizzando una caldaia per acqua calda, inserisci l'Impostazione della Temperatura della Caldaia con il valore appropriato. Per i sistemi di riscaldamento elettrici, inserisci il valore 100.\n\nQueste impostazioni sono essenziali per un controllo preciso della temperatura e per garantire prestazioni ottimali del tuo sistema di riscaldamento. Fornire l'Impostazione della Temperatura corretta consente una regolazione accurata e contribuisce a creare un ambiente confortevole ed efficiente dal punto di vista energetico nella tua casa.", + "title": "Termostato PID con PWM (ON/OFF)" + }, + "user": { + "description": "Il SAT è un termostato intelligente capace di auto-regolarsi per ottimizzare il controllo della temperatura. Seleziona la modalità appropriata che corrisponde al tuo sistema di riscaldamento.", "menu_options": { - "calibrate": "Riprova la calibrazione", - "finish": "Continua con la calibrazione attuale" - } + "mosquitto": "Gateway OpenTherm (MQTT)", + "serial": "Gateway OpenTherm (SERIALE)", + "simulator": "Gateway Simulato (AVANZATO)", + "switch": "Termostato PID con PWM (ON/OFF)" + }, + "title": "Smart Autotune Thermostat (SAT)" } - }, - "error": { - "connection": "Impossibile connettersi al gateway.", - "mqtt_component": "Il componente MQTT non è disponibile.", - "unable_to_calibrate": "Il processo di calibrazione ha incontrato un problema e non è stato completato con successo. Si prega di assicurarsi che il sistema di riscaldamento funzioni correttamente e che tutti i sensori richiesti siano connessi e funzionanti correttamente.\n\nSe continui a riscontrare problemi con la calibrazione, considera di contattarci per ulteriore assistenza. Ci scusiamo per eventuali inconvenienti causati." - }, - "abort": { - "already_configured": "Il gateway è già configurato." - }, - "progress": { - "calibration": "Calibrazione e ricerca del valore di protezione dal superamento in corso...\n\nSi prega di attendere mentre ottimizziamo il tuo sistema di riscaldamento. Questo processo può richiedere circa 20 minuti." } }, "options": { "step": { - "init": { - "menu_options": { - "general": "Generale", - "presets": "Preimpostazioni", - "advanced": "Opzioni Avanzate", - "system_configuration": "Configurazione del Sistema" - } + "advanced": { + "data": { + "climate_valve_offset": "Offset della Valvola Climatica", + "dynamic_minimum_setpoint": "Setpoint Minimo Dinamico (Sperimentale)", + "force_pulse_width_modulation": "Forzare la Modulazione di Larghezza di Impulso", + "maximum_consumption": "Consumo Massimo", + "maximum_relative_modulation": "Modulazione Relativa Massima", + "minimum_consumption": "Consumo Minimo", + "sample_time": "Tempo di Campionamento", + "simulation": "Simulazione", + "target_temperature_step": "Passo della Temperatura Target", + "thermal_comfort": "Comfort Termico" + }, + "data_description": { + "climate_valve_offset": "Offset per regolare il grado di apertura della valvola climatica.", + "dynamic_minimum_setpoint": "Attiva la regolazione dinamica del punto di impostazione minimo in base alla temperatura di ritorno della caldaia, che aiuta anche a identificare se delle valvole sono chiuse.", + "maximum_consumption": "Il consumo massimo di gas quando la caldaia è attiva.", + "maximum_relative_modulation": "Rappresenta il livello di modulazione più alto per un sistema di riscaldamento efficiente.", + "minimum_consumption": "Il consumo minimo di gas quando la caldaia è attiva.", + "sample_time": "L'intervallo di tempo minimo tra gli aggiornamenti del controllore PID.", + "target_temperature_step": "Regolare il passo della temperatura target per un'accurata regolazione dei livelli di comfort.", + "thermal_comfort": "Abilitare l'uso dell'Indice di Simmer per l'aggiustamento del comfort termico." + }, + "title": "Avanzate" }, "general": { - "title": "Generale", - "description": "Impostazioni e configurazioni generali.", "data": { - "integral": "Integrale (kI)", - "derivative": "Derivata (kD)", - "proportional": "Proporzionale (kP)", - "maximum_setpoint": "Setpoint Massimo", - "window_sensors": "Sensori Contatto", "automatic_gains_value": "Valore dei Guadagni Automatici", + "derivative": "Derivata (kD)", "derivative_time_weight": "Peso Temporale Derivato", - "heating_curve_version": "Versione della Curva di Riscaldamento", - "heating_curve_coefficient": "Coefficiente della Curva di Riscaldamento", "duty_cycle": "Ciclo di Lavoro Massimo per la Modulazione di Larghezza di Impulso", - "sync_with_thermostat": "Sincronizza setpoint con termostato" + "heating_curve_coefficient": "Coefficiente della Curva di Riscaldamento", + "heating_curve_version": "Versione della Curva di Riscaldamento", + "integral": "Integrale (kI)", + "maximum_setpoint": "Setpoint Massimo", + "minimum_setpoint_adjustment_factor": "Fattore di regolazione del punto di impostazione minimo", + "pid_controller_version": "Versione del controllore PID", + "proportional": "Proporzionale (kP)", + "sync_with_thermostat": "Sincronizza setpoint con termostato", + "window_sensors": "Sensori Contatto" }, "data_description": { - "integral": "Il termine integrale (kI) nel controllore PID, responsabile della riduzione dell'errore in stato stazionario.", - "derivative": "Il termine derivato (kD) nel controllore PID, responsabile della mitigazione del superamento.", - "proportional": "Il termine proporzionale (kP) nel controllore PID, responsabile della risposta immediata agli errori.", - "maximum_setpoint": "La temperatura ottimale per un funzionamento efficiente della caldaia.", - "window_sensors": "Sensori di Contatto che attivano il sistema quando una finestra o una porta è aperta per un periodo di tempo.", "automatic_gains_value": "Il valore utilizzato per i guadagni automatici nel controllore PID.", - "heating_curve_coefficient": "Il coefficiente utilizzato per regolare la curva di riscaldamento.", + "derivative": "Il termine derivato (kD) nel controllore PID, responsabile della mitigazione del superamento.", + "derivative_time_weight": "Un parametro per regolare l'influenza del termine derivato nel tempo, particolarmente utile per ridurre il sottoscavo durante la fase di riscaldamento quando il coefficiente della curva di riscaldamento è correttamente impostato.", "duty_cycle": "Il ciclo di lavoro massimo per la Modulazione di Larghezza di Impulso (PWM), controllando i cicli di accensione/spegnimento della caldaia.", + "heating_curve_coefficient": "Il coefficiente utilizzato per regolare la curva di riscaldamento.", + "integral": "Il termine integrale (kI) nel controllore PID, responsabile della riduzione dell'errore in stato stazionario.", + "maximum_setpoint": "La temperatura ottimale per un funzionamento efficiente della caldaia.", + "minimum_setpoint_adjustment_factor": "Questo fattore regola il punto di impostazione del riscaldamento in base alla temperatura di ritorno della caldaia, influenzando la reattività e l'efficienza del riscaldamento. Un valore più alto aumenta la sensibilità ai cambiamenti di temperatura, migliorando il controllo del comfort e del consumo energetico. L'intervallo di partenza consigliato è da 0,1 a 0,5. Regolare per adattarsi al proprio sistema e alle preferenze di comfort.", + "proportional": "Il termine proporzionale (kP) nel controllore PID, responsabile della risposta immediata agli errori.", "sync_with_thermostat": "Sincronizza il setpoint con il termostato per garantire un controllo coordinato della temperatura.", - "derivative_time_weight": "Un parametro per regolare l'influenza del termine derivato nel tempo, particolarmente utile per ridurre il sottoscavo durante la fase di riscaldamento quando il coefficiente della curva di riscaldamento è correttamente impostato." + "window_sensors": "Sensori di Contatto che attivano il sistema quando una finestra o una porta è aperta per un periodo di tempo." + }, + "description": "Impostazioni e configurazioni generali.", + "title": "Generale" + }, + "init": { + "menu_options": { + "advanced": "Opzioni Avanzate", + "general": "Generale", + "presets": "Preimpostazioni", + "system_configuration": "Configurazione del Sistema" } }, "presets": { - "title": "Preimpostazioni", - "description": "Impostazioni di temperatura predefinite per diversi scenari o attività.", "data": { + "activity_temperature": "Temperatura Attività", "away_temperature": "Temperatura Assente", + "comfort_temperature": "Temperatura Comfort", "home_temperature": "Temperatura Casa", "sleep_temperature": "Temperatura Sonno", - "comfort_temperature": "Temperatura Comfort", - "activity_temperature": "Temperatura Attività", "sync_climates_with_preset": "Sincronizza climi con preimpostazione (sonno / assente / attività)" - } + }, + "description": "Impostazioni di temperatura predefinite per diversi scenari o attività.", + "title": "Preimpostazioni" }, "system_configuration": { - "title": "Configurazione del Sistema", - "description": "Per una regolazione fine e una personalizzazione.", "data": { "automatic_duty_cycle": "Ciclo di lavoro automatico", "overshoot_protection": "Protezione dal Superamento (con PWM)", - "window_minimum_open_time": "Tempo minimo di apertura della finestra", - "sensor_max_value_age": "Età massima del valore del sensore di temperatura" + "sensor_max_value_age": "Età massima del valore del sensore di temperatura", + "window_minimum_open_time": "Tempo minimo di apertura della finestra" }, "data_description": { "automatic_duty_cycle": "Abilitare o disabilitare il ciclo di lavoro automatico per la Modulazione di Larghezza di Impulso (PWM).", "overshoot_protection": "Abilitare la protezione dal superamento con la Modulazione di Larghezza di Impulso (PWM) per prevenire il superamento della temperatura della caldaia.", - "window_minimum_open_time": "Il tempo minimo che una finestra deve essere aperta prima che il sistema reagisca.", - "sensor_max_value_age": "L'età massima del valore del sensore di temperatura prima di considerarlo stazionario." - } - }, - "advanced": { - "title": "Avanzate", - "data": { - "simulation": "Simulazione", - "sample_time": "Tempo di Campionamento", - "thermal_comfort": "Comfort Termico", - "minimum_consumption": "Consumo Minimo", - "maximum_consumption": "Consumo Massimo", - "climate_valve_offset": "Offset della Valvola Climatica", - "target_temperature_step": "Passo della Temperatura Target", - "maximum_relative_modulation": "Modulazione Relativa Massima", - "force_pulse_width_modulation": "Forzare la Modulazione di Larghezza di Impulso", - "dynamic_minimum_setpoint": "Setpoint Minimo Dinamico (Sperimentale)" + "sensor_max_value_age": "L'età massima del valore del sensore di temperatura prima di considerarlo stazionario.", + "window_minimum_open_time": "Il tempo minimo che una finestra deve essere aperta prima che il sistema reagisca." }, - "data_description": { - "thermal_comfort": "Abilitare l'uso dell'Indice di Simmer per l'aggiustamento del comfort termico.", - "minimum_consumption": "Il consumo minimo di gas quando la caldaia è attiva.", - "maximum_consumption": "Il consumo massimo di gas quando la caldaia è attiva.", - "climate_valve_offset": "Offset per regolare il grado di apertura della valvola climatica.", - "target_temperature_step": "Regolare il passo della temperatura target per un'accurata regolazione dei livelli di comfort.", - "sample_time": "L'intervallo di tempo minimo tra gli aggiornamenti del controllore PID.", - "maximum_relative_modulation": "Rappresenta il livello di modulazione più alto per un sistema di riscaldamento efficiente." - } + "description": "Per una regolazione fine e una personalizzazione.", + "title": "Configurazione del Sistema" } } } diff --git a/custom_components/sat/translations/nl.json b/custom_components/sat/translations/nl.json index f86881f2..e51f997c 100644 --- a/custom_components/sat/translations/nl.json +++ b/custom_components/sat/translations/nl.json @@ -1,221 +1,225 @@ { "config": { + "abort": { + "already_configured": "Gateway is al geconfigureerd." + }, + "error": { + "connection": "Kan geen verbinding maken met de gateway.", + "mqtt_component": "Het MQTT-component is niet beschikbaar.", + "unable_to_calibrate": "Het kalibratieproces is op een probleem gestuit en kon niet succesvol worden voltooid. Zorg ervoor dat uw verwarmingssysteem correct functioneert en dat alle vereiste sensoren zijn aangesloten en correct werken.\n\nAls u problemen blijft ondervinden met de kalibratie, overweeg dan om contact met ons op te nemen voor verdere hulp. Onze excuses voor het ongemak." + }, + "progress": { + "calibration": "Kalibreren en de waarde van de overshootbeveiliging vinden...\n\nEven geduld a.u.b. terwijl wij uw verwarmingssysteem optimaliseren. Dit proces kan ongeveer 20 minuten duren." + }, "step": { - "user": { - "title": "Slimme Autotune Thermostaat (SAT)", - "description": "SAT is een slimme thermostaat die zichzelf kan afstemmen om de temperatuurregeling te optimaliseren. Selecteer de geschikte modus die overeenkomt met uw verwarmingssysteem.", - "menu_options": { - "mosquitto": "OpenTherm Gateway ( MQTT )", - "serial": "OpenTherm Gateway ( SERIEEL )", - "switch": "PID Thermostaat met PWM ( AAN/UIT )", - "simulator": "Gesimuleerde Gateway ( GEAVANCEERD )" - } - }, - "mosquitto": { - "title": "OpenTherm Gateway ( MQTT )", - "description": "Gelieve de volgende gegevens te verstrekken om de OpenTherm Gateway in te stellen. Voer in het veld Naam een naam in voor de gateway die u helpt deze te identificeren binnen uw systeem.\n\nSpecificeer de Climate entity die gebruikt wordt voor de OpenTherm Gateway. Deze entiteit wordt aangeleverd door de OpenTherm Gateway en vertegenwoordigt uw verwarmingssysteem.\n\nVoer daarnaast het Top Topic in dat gebruikt zal worden voor het publiceren en abonneren op MQTT-berichten gerelateerd aan de OpenTherm Gateway.\n\nDeze instellingen zijn essentieel voor het tot stand brengen van communicatie en integratie met uw OpenTherm Gateway via MQTT. Ze zorgen voor een naadloze gegevensuitwisseling en controle over uw verwarmingssysteem. Zorg ervoor dat de verstrekte gegevens nauwkeurig zijn om een correcte werking te garanderen.", - "data": { - "name": "Naam", - "device": "Apparaat", - "mqtt_topic": "Top Topic" - } - }, - "serial": { - "title": "OpenTherm Gateway ( SERIEEL )", - "description": "Om een verbinding met de OpenTherm Gateway te maken via een socketverbinding, gelieve de volgende details te verstrekken. Voer in het veld Naam een naam in voor de gateway die u helpt deze te identificeren binnen uw systeem.\n\nSpecificeer het netwerkadres van de OpenTherm Gateway in het veld Apparaat. Dit kan in het formaat zijn van \"socket://otgw.local:25238\", waarbij \"otgw.local\" de hostnaam of het IP-adres van de gateway is en \"25238\" het poortnummer.\n\nDeze instellingen zijn essentieel voor het tot stand brengen van communicatie en integratie met uw OpenTherm Gateway via de socketverbinding. Zorg ervoor dat de verstrekte gegevens nauwkeurig zijn om een correcte werking te garanderen.", - "data": { - "name": "Naam", - "device": "URL" - } - }, - "switch": { - "title": "PID Thermostaat met PWM ( AAN/UIT )", - "description": "Gelieve de volgende details in te vullen om de schakelaar in te stellen. Voer in het veld Naam een naam in voor de schakelaar, die u helpt deze te identificeren binnen uw systeem. Kies de geschikte entiteit om voor uw schakelaar te gebruiken uit de aangeboden opties.\n\nIn het veld Temperatuurinstelling specificeert u de gewenste doeltemperatuur voor uw verwarmingssysteem. Als u een warmwaterboiler gebruikt, vul dan de Boiler Temperatuurinstelling in met de geschikte waarde. Voor elektrische verwarmingssystemen, voer de waarde 100 in.\n\nDeze instellingen zijn essentieel voor precieze temperatuurregeling en zorgen voor optimale prestaties van uw verwarmingssysteem. Het verstrekken van de juiste Temperatuurinstelling zorgt voor nauwkeurige regulatie en helpt een comfortabele en energie-efficiënte omgeving in uw huis te bereiken.", - "data": { - "name": "Naam", - "device": "Entiteit", - "minimum_setpoint": "Temperatuurinstelling" - } - }, - "simulator": { - "title": "Gesimuleerde Gateway ( GEAVANCEERD )", - "description": "Deze gateway stelt u in staat om een ketel te simuleren voor test- en demonstratiedoeleinden. Gelieve de volgende informatie te verstrekken om de simulator te configureren.\n\nLet op: De Simulator Gateway is alleen bedoeld voor test- en demonstratiedoeleinden en mag niet worden gebruikt in productieomgevingen.", - "data": { - "name": "Naam", - "minimum_setpoint": "Minimum Setpoint", - "maximum_setpoint": "Maximum Setpoint", - "simulated_heating": "Gesimuleerde Verwarming", - "simulated_cooling": "Gesimuleerde Koeling", - "simulated_warming_up": "Gesimuleerde Opwarming" - } - }, - "sensors": { - "title": "Configureer sensoren", - "description": "Selecteer de sensoren die gebruikt zullen worden om de temperatuur te volgen.", - "data": { - "inside_sensor_entity_id": "Binnensensor Entiteit", - "outside_sensor_entity_id": "Buitensensor Entiteit", - "humidity_sensor_entity_id": "Vochtigheidssensor Entiteit" - } - }, - "heating_system": { - "title": "Verwarmingssysteem", - "description": "Het kiezen van het juiste type verwarmingssysteem is belangrijk voor SAT om de temperatuur nauwkeurig te regelen en prestaties te optimaliseren. Kies de optie die overeenkomt met uw opstelling om een correcte temperatuurregeling in uw huis te verzekeren.", - "data": { - "heating_system": "Systeem" - } - }, "areas": { - "title": "Gebieden", - "description": "Instellingen met betrekking tot klimaten, multi-kamer en temperatuurregeling. Primaire klimaten bevinden zich in dezelfde kamer als de binnensensor en de kamers hebben hun eigen doeltemperaturen los van het systeem.", "data": { "main_climates": "Primair", "secondary_climates": "Kamers" - } + }, + "description": "Instellingen met betrekking tot klimaten, multi-kamer en temperatuurregeling. Primaire klimaten bevinden zich in dezelfde kamer als de binnensensor en de kamers hebben hun eigen doeltemperaturen los van het systeem.", + "title": "Gebieden" }, "automatic_gains": { - "title": "Automatische Versterkingen", - "description": "Deze functie past de regelparameters van uw verwarmingssysteem dynamisch aan, waardoor de temperatuurregeling wordt geoptimaliseerd voor meer comfort en energie-efficiëntie. Het inschakelen van deze optie stelt SAT in staat om de verwarmingsinstellingen voortdurend aan te passen en te verfijnen op basis van de omgevingsomstandigheden. Dit helpt een stabiele en comfortabele omgeving te handhaven zonder handmatige tussenkomst.\n\nLet op: Als u ervoor kiest om automatische versterkingen niet in te schakelen, moet u handmatig de PID-waarden invoeren voor nauwkeurige temperatuurregeling. Zorg ervoor dat u nauwkeurige PID-waarden heeft voor uw specifieke verwarmingssysteem om optimale prestaties te bereiken.", "data": { "automatic_gains": "Automatische Versterkingen (aanbevolen)" - } + }, + "description": "Deze functie past de regelparameters van uw verwarmingssysteem dynamisch aan, waardoor de temperatuurregeling wordt geoptimaliseerd voor meer comfort en energie-efficiëntie. Het inschakelen van deze optie stelt SAT in staat om de verwarmingsinstellingen voortdurend aan te passen en te verfijnen op basis van de omgevingsomstandigheden. Dit helpt een stabiele en comfortabele omgeving te handhaven zonder handmatige tussenkomst.\n\nLet op: Als u ervoor kiest om automatische versterkingen niet in te schakelen, moet u handmatig de PID-waarden invoeren voor nauwkeurige temperatuurregeling. Zorg ervoor dat u nauwkeurige PID-waarden heeft voor uw specifieke verwarmingssysteem om optimale prestaties te bereiken.", + "title": "Automatische Versterkingen" }, "calibrate_system": { - "title": "Systeem Kalibreren", "description": "Optimaliseer uw verwarmingssysteem door automatisch de optimale PID-waarden voor uw opstelling te bepalen. Let op dat het systeem bij het selecteren van Automatische Versterkingen een kalibratieproces zal ondergaan dat ongeveer 20 minuten kan duren.\n\nAutomatische Versterkingen worden aanbevolen voor de meeste gebruikers omdat het het instelproces vereenvoudigt en optimale prestaties verzekert. Echter, als u bekend bent met PID-regeling en de voorkeur geeft aan het handmatig instellen van de waarden, kunt u ervoor kiezen om Automatische Versterkingen over te slaan.\n\nLet op dat het kiezen om Automatische Versterkingen over te slaan een goede kennis van PID-regeling vereist en mogelijk extra handmatige aanpassingen nodig heeft om optimale prestaties te bereiken.", "menu_options": { "calibrate": "Kalibreer en bepaal uw overshoot-beveiligingswaarde (ong. 20 min).", "overshoot_protection": "Voer handmatig de overshoot-beveiligingswaarde in.", "pid_controller": "Voer handmatig PID-waarden in (niet aanbevolen)." - } + }, + "title": "Systeem Kalibreren" + }, + "calibrated": { + "description": "Het kalibratieproces is succesvol voltooid.\n\nGefeliciteerd! Uw Slimme Autotune Thermostaat (SAT) is gekalibreerd om de verwarmingsprestaties van uw systeem te optimaliseren. Tijdens het kalibratieproces heeft SAT zorgvuldig de verwarmingskenmerken geanalyseerd en de geschikte overshoot-beveiligingswaarde bepaald om nauwkeurige temperatuurregeling te verzekeren.\n\nOvershoot-beveiligingswaarde: {minimum_setpoint} °C\n\nDeze waarde vertegenwoordigt de maximale hoeveelheid overshoot die is toegestaan tijdens het verwarmingsproces. SAT zal actief de verwarming monitoren en aanpassen om overmatig overshoot te voorkomen, en zo een comfortabele en efficiënte verwarmingservaring in uw huis te handhaven.\n\nLet op dat de overshoot-beveiligingswaarde kan variëren afhankelijk van de specifieke kenmerken van uw verwarmingssysteem en omgevingsfactoren. Het is fijn afgestemd om optimale prestaties te bieden op basis van de kalibratieresultaten.", + "menu_options": { + "calibrate": "Kalibratie opnieuw proberen", + "finish": "Doorgaan met huidige kalibratie" + }, + "title": "Kalibratie Voltooid" + }, + "heating_system": { + "data": { + "heating_system": "Systeem" + }, + "description": "Het kiezen van het juiste type verwarmingssysteem is belangrijk voor SAT om de temperatuur nauwkeurig te regelen en prestaties te optimaliseren. Kies de optie die overeenkomt met uw opstelling om een correcte temperatuurregeling in uw huis te verzekeren.", + "title": "Verwarmingssysteem" + }, + "mosquitto": { + "data": { + "device": "Apparaat", + "mqtt_topic": "Top Topic", + "name": "Naam" + }, + "description": "Gelieve de volgende gegevens te verstrekken om de OpenTherm Gateway in te stellen. Voer in het veld Naam een naam in voor de gateway die u helpt deze te identificeren binnen uw systeem.\n\nSpecificeer de Climate entity die gebruikt wordt voor de OpenTherm Gateway. Deze entiteit wordt aangeleverd door de OpenTherm Gateway en vertegenwoordigt uw verwarmingssysteem.\n\nVoer daarnaast het Top Topic in dat gebruikt zal worden voor het publiceren en abonneren op MQTT-berichten gerelateerd aan de OpenTherm Gateway.\n\nDeze instellingen zijn essentieel voor het tot stand brengen van communicatie en integratie met uw OpenTherm Gateway via MQTT. Ze zorgen voor een naadloze gegevensuitwisseling en controle over uw verwarmingssysteem. Zorg ervoor dat de verstrekte gegevens nauwkeurig zijn om een correcte werking te garanderen.", + "title": "OpenTherm Gateway ( MQTT )" }, "overshoot_protection": { - "title": "Overshoot-beveiliging", - "description": "Door het verstrekken van de overshoot-beveiligingswaarde zal SAT de regelparameters dienovereenkomstig aanpassen om een stabiele en comfortabele verwarmingsomgeving te behouden. Deze handmatige configuratie stelt u in staat het systeem fijn af te stemmen op basis van uw specifieke opstelling.\n\nLet op: Als u niet zeker bent over de overshoot-beveiligingswaarde of het kalibratieproces niet heeft uitgevoerd, wordt aanbevolen de configuratie te annuleren en het kalibratieproces te doorlopen zodat SAT automatisch de waarde voor optimale prestaties kan bepalen.", "data": { "minimum_setpoint": "Waarde" - } + }, + "description": "Door het verstrekken van de overshoot-beveiligingswaarde zal SAT de regelparameters dienovereenkomstig aanpassen om een stabiele en comfortabele verwarmingsomgeving te behouden. Deze handmatige configuratie stelt u in staat het systeem fijn af te stemmen op basis van uw specifieke opstelling.\n\nLet op: Als u niet zeker bent over de overshoot-beveiligingswaarde of het kalibratieproces niet heeft uitgevoerd, wordt aanbevolen de configuratie te annuleren en het kalibratieproces te doorlopen zodat SAT automatisch de waarde voor optimale prestaties kan bepalen.", + "title": "Overshoot-beveiliging" }, "pid_controller": { - "title": "Configureer de PID-regelaar handmatig.", - "description": "Configureer de proportionele, integrale en afgeleide versterkingen handmatig om uw verwarmingssysteem fijn te stemmen. Gebruik deze optie als u volledige controle wilt hebben over de PID-regelaarparameters. Stel de versterkingen in op basis van de specifieke kenmerken van uw verwarmingssysteem en voorkeuren.", "data": { - "integral": "Integraal (kI)", "derivative": "Afgeleid (kD)", + "integral": "Integraal (kI)", "proportional": "Proportioneel (kP)" - } + }, + "description": "Configureer de proportionele, integrale en afgeleide versterkingen handmatig om uw verwarmingssysteem fijn te stemmen. Gebruik deze optie als u volledige controle wilt hebben over de PID-regelaarparameters. Stel de versterkingen in op basis van de specifieke kenmerken van uw verwarmingssysteem en voorkeuren.", + "title": "Configureer de PID-regelaar handmatig." }, - "calibrated": { - "title": "Kalibratie Voltooid", - "description": "Het kalibratieproces is succesvol voltooid.\n\nGefeliciteerd! Uw Slimme Autotune Thermostaat (SAT) is gekalibreerd om de verwarmingsprestaties van uw systeem te optimaliseren. Tijdens het kalibratieproces heeft SAT zorgvuldig de verwarmingskenmerken geanalyseerd en de geschikte overshoot-beveiligingswaarde bepaald om nauwkeurige temperatuurregeling te verzekeren.\n\nOvershoot-beveiligingswaarde: {minimum_setpoint} °C\n\nDeze waarde vertegenwoordigt de maximale hoeveelheid overshoot die is toegestaan tijdens het verwarmingsproces. SAT zal actief de verwarming monitoren en aanpassen om overmatig overshoot te voorkomen, en zo een comfortabele en efficiënte verwarmingservaring in uw huis te handhaven.\n\nLet op dat de overshoot-beveiligingswaarde kan variëren afhankelijk van de specifieke kenmerken van uw verwarmingssysteem en omgevingsfactoren. Het is fijn afgestemd om optimale prestaties te bieden op basis van de kalibratieresultaten.", + "sensors": { + "data": { + "humidity_sensor_entity_id": "Vochtigheidssensor Entiteit", + "inside_sensor_entity_id": "Binnensensor Entiteit", + "outside_sensor_entity_id": "Buitensensor Entiteit" + }, + "description": "Selecteer de sensoren die gebruikt zullen worden om de temperatuur te volgen.", + "title": "Configureer sensoren" + }, + "serial": { + "data": { + "device": "URL", + "name": "Naam" + }, + "description": "Om een verbinding met de OpenTherm Gateway te maken via een socketverbinding, gelieve de volgende details te verstrekken. Voer in het veld Naam een naam in voor de gateway die u helpt deze te identificeren binnen uw systeem.\n\nSpecificeer het netwerkadres van de OpenTherm Gateway in het veld Apparaat. Dit kan in het formaat zijn van \"socket://otgw.local:25238\", waarbij \"otgw.local\" de hostnaam of het IP-adres van de gateway is en \"25238\" het poortnummer.\n\nDeze instellingen zijn essentieel voor het tot stand brengen van communicatie en integratie met uw OpenTherm Gateway via de socketverbinding. Zorg ervoor dat de verstrekte gegevens nauwkeurig zijn om een correcte werking te garanderen.", + "title": "OpenTherm Gateway ( SERIEEL )" + }, + "simulator": { + "data": { + "maximum_setpoint": "Maximum Setpoint", + "minimum_setpoint": "Minimum Setpoint", + "name": "Naam", + "simulated_cooling": "Gesimuleerde Koeling", + "simulated_heating": "Gesimuleerde Verwarming", + "simulated_warming_up": "Gesimuleerde Opwarming" + }, + "description": "Deze gateway stelt u in staat om een ketel te simuleren voor test- en demonstratiedoeleinden. Gelieve de volgende informatie te verstrekken om de simulator te configureren.\n\nLet op: De Simulator Gateway is alleen bedoeld voor test- en demonstratiedoeleinden en mag niet worden gebruikt in productieomgevingen.", + "title": "Gesimuleerde Gateway ( GEAVANCEERD )" + }, + "switch": { + "data": { + "device": "Entiteit", + "minimum_setpoint": "Temperatuurinstelling", + "name": "Naam" + }, + "description": "Gelieve de volgende details in te vullen om de schakelaar in te stellen. Voer in het veld Naam een naam in voor de schakelaar, die u helpt deze te identificeren binnen uw systeem. Kies de geschikte entiteit om voor uw schakelaar te gebruiken uit de aangeboden opties.\n\nIn het veld Temperatuurinstelling specificeert u de gewenste doeltemperatuur voor uw verwarmingssysteem. Als u een warmwaterboiler gebruikt, vul dan de Boiler Temperatuurinstelling in met de geschikte waarde. Voor elektrische verwarmingssystemen, voer de waarde 100 in.\n\nDeze instellingen zijn essentieel voor precieze temperatuurregeling en zorgen voor optimale prestaties van uw verwarmingssysteem. Het verstrekken van de juiste Temperatuurinstelling zorgt voor nauwkeurige regulatie en helpt een comfortabele en energie-efficiënte omgeving in uw huis te bereiken.", + "title": "PID Thermostaat met PWM ( AAN/UIT )" + }, + "user": { + "description": "SAT is een slimme thermostaat die zichzelf kan afstemmen om de temperatuurregeling te optimaliseren. Selecteer de geschikte modus die overeenkomt met uw verwarmingssysteem.", "menu_options": { - "calibrate": "Kalibratie opnieuw proberen", - "finish": "Doorgaan met huidige kalibratie" - } + "mosquitto": "OpenTherm Gateway ( MQTT )", + "serial": "OpenTherm Gateway ( SERIEEL )", + "simulator": "Gesimuleerde Gateway ( GEAVANCEERD )", + "switch": "PID Thermostaat met PWM ( AAN/UIT )" + }, + "title": "Slimme Autotune Thermostaat (SAT)" } - }, - "error": { - "connection": "Kan geen verbinding maken met de gateway.", - "mqtt_component": "Het MQTT-component is niet beschikbaar.", - "unable_to_calibrate": "Het kalibratieproces is op een probleem gestuit en kon niet succesvol worden voltooid. Zorg ervoor dat uw verwarmingssysteem correct functioneert en dat alle vereiste sensoren zijn aangesloten en correct werken.\n\nAls u problemen blijft ondervinden met de kalibratie, overweeg dan om contact met ons op te nemen voor verdere hulp. Onze excuses voor het ongemak." - }, - "abort": { - "already_configured": "Gateway is al geconfigureerd." - }, - "progress": { - "calibration": "Kalibreren en de waarde van de overshootbeveiliging vinden...\n\nEven geduld a.u.b. terwijl wij uw verwarmingssysteem optimaliseren. Dit proces kan ongeveer 20 minuten duren." } }, "options": { "step": { - "init": { - "menu_options": { - "general": "Algemeen", - "presets": "Voorinstellingen", - "advanced": "Geavanceerde Opties", - "system_configuration": "Systeemconfiguratie" - } + "advanced": { + "data": { + "climate_valve_offset": "Offset van Klimaatklep", + "dynamic_minimum_setpoint": "Dynamisch Minimaal Setpoint (Experimenteel)", + "force_pulse_width_modulation": "Dwing Pulsbreedtemodulatie af", + "maximum_consumption": "Maximaal Verbruik", + "maximum_relative_modulation": "Maximale Relatieve Modulatie", + "minimum_consumption": "Minimaal Verbruik", + "sample_time": "Sampletijd", + "simulation": "Simulatie", + "target_temperature_step": "Stap van Doeltemperatuur", + "thermal_comfort": "Thermisch Comfort" + }, + "data_description": { + "climate_valve_offset": "Offset om de openingsgraad van de klimaatklep aan te passen.", + "dynamic_minimum_setpoint": "Activeert de dynamische aanpassing van de minimale instelwaarde op basis van de retourtemperatuur van de ketel, wat ook helpt te identificeren of er kleppen gesloten zijn.", + "maximum_consumption": "Het maximale gasverbruik wanneer de ketel actief is.", + "maximum_relative_modulation": "Vertegenwoordigt het hoogste modulatieniveau voor een efficiënt verwarmingssysteem.", + "minimum_consumption": "Het minimale gasverbruik wanneer de ketel actief is.", + "sample_time": "Het minimale tijdsinterval tussen updates aan de PID-regelaar.", + "target_temperature_step": "De stap van de doeltemperatuur aanpassen voor fijnafstemming van comfortniveaus.", + "thermal_comfort": "Gebruik van de Simmer Index voor aanpassing van thermisch comfort inschakelen." + }, + "title": "Geavanceerd" }, "general": { - "title": "Algemeen", - "description": "Algemene instellingen en configuraties.", "data": { - "integral": "Integraal (kI)", - "derivative": "Afgeleide (kD)", - "proportional": "Proportioneel (kP)", - "maximum_setpoint": "Maximaal Setpoint", - "window_sensors": "Contact Sensoren", "automatic_gains_value": "Automatische Versterkingswaarde", + "derivative": "Afgeleide (kD)", "derivative_time_weight": "Tijdgewicht van de Afgeleide", - "heating_curve_version": "Versie van de Verwarmingscurve", - "heating_curve_coefficient": "Coëfficiënt van de Verwarmingscurve", "duty_cycle": "Maximale Inschakelduur voor Pulsbreedtemodulatie", - "sync_with_thermostat": "Synchroniseer setpoint met thermostaat" + "heating_curve_coefficient": "Coëfficiënt van de Verwarmingscurve", + "heating_curve_version": "Versie van de Verwarmingscurve", + "integral": "Integraal (kI)", + "maximum_setpoint": "Maximaal Setpoint", + "minimum_setpoint_adjustment_factor": "Aanpassingsfactor voor de minimale instelwaarde", + "pid_controller_version": "Versie van de PID-regelaar", + "proportional": "Proportioneel (kP)", + "sync_with_thermostat": "Synchroniseer setpoint met thermostaat", + "window_sensors": "Contact Sensoren" }, "data_description": { - "integral": "De integraalterm (kI) in de PID-regelaar, verantwoordelijk voor het verminderen van de blijvende fout.", - "derivative": "De afgeleideterm (kD) in de PID-regelaar, verantwoordelijk voor het verminderen van overshoot.", - "proportional": "De proportionele term (kP) in de PID-regelaar, verantwoordelijk voor de directe reactie op fouten.", - "maximum_setpoint": "De optimale temperatuur voor efficiënte werking van de ketel.", - "window_sensors": "Contact Sensoren die het systeem activeren wanneer een raam of deur voor een periode geopend is.", "automatic_gains_value": "De waarde die wordt gebruikt voor automatische versterkingen in de PID-regelaar.", - "heating_curve_coefficient": "De coëfficiënt die wordt gebruikt om de verwarmingscurve aan te passen.", + "derivative": "De afgeleideterm (kD) in de PID-regelaar, verantwoordelijk voor het verminderen van overshoot.", + "derivative_time_weight": "Een parameter om de invloed van de afgeleideterm over tijd aan te passen, vooral nuttig om undershoot te verminderen tijdens de opwarmfase wanneer de coëfficiënt van de verwarmingscurve correct is ingesteld.", "duty_cycle": "De maximale inschakelduur voor Pulsbreedtemodulatie (PWM), die de aan/uit-cycli van de ketel regelt.", + "heating_curve_coefficient": "De coëfficiënt die wordt gebruikt om de verwarmingscurve aan te passen.", + "integral": "De integraalterm (kI) in de PID-regelaar, verantwoordelijk voor het verminderen van de blijvende fout.", + "maximum_setpoint": "De optimale temperatuur voor efficiënte werking van de ketel.", + "minimum_setpoint_adjustment_factor": "Deze factor past de instelwaarde voor verwarming aan op basis van de retourtemperatuur van de ketel, wat de responsiviteit en efficiëntie van de verwarming beïnvloedt. Een hogere waarde verhoogt de gevoeligheid voor temperatuurveranderingen, waardoor de controle over comfort en energieverbruik wordt verbeterd. Het aanbevolen startbereik is 0,1 tot 0,5. Aanpassen om aan uw systeem en comfortvoorkeuren te voldoen.", + "proportional": "De proportionele term (kP) in de PID-regelaar, verantwoordelijk voor de directe reactie op fouten.", "sync_with_thermostat": "Synchroniseer het setpoint met de thermostaat om een gecoördineerde temperatuurregeling te waarborgen.", - "derivative_time_weight": "Een parameter om de invloed van de afgeleideterm over tijd aan te passen, vooral nuttig om undershoot te verminderen tijdens de opwarmfase wanneer de coëfficiënt van de verwarmingscurve correct is ingesteld." + "window_sensors": "Contact Sensoren die het systeem activeren wanneer een raam of deur voor een periode geopend is." + }, + "description": "Algemene instellingen en configuraties.", + "title": "Algemeen" + }, + "init": { + "menu_options": { + "advanced": "Geavanceerde Opties", + "general": "Algemeen", + "presets": "Voorinstellingen", + "system_configuration": "Systeemconfiguratie" } }, "presets": { - "title": "Voorinstellingen", - "description": "Vooraf gedefinieerde temperatuurinstellingen voor verschillende scenario's of activiteiten.", "data": { + "activity_temperature": "Activiteit Temperatuur", "away_temperature": "Temperatuur bij Afwezigheid", + "comfort_temperature": "Comfort Temperatuur", "home_temperature": "Thuis Temperatuur", "sleep_temperature": "Slaap Temperatuur", - "comfort_temperature": "Comfort Temperatuur", - "activity_temperature": "Activiteit Temperatuur", "sync_climates_with_preset": "Synchroniseer klimaten met voorinstelling (slaap / afwezig / activiteit)" - } + }, + "description": "Vooraf gedefinieerde temperatuurinstellingen voor verschillende scenario's of activiteiten.", + "title": "Voorinstellingen" }, "system_configuration": { - "title": "Systeemconfiguratie", - "description": "Voor fijnafstelling en aanpassing.", "data": { "automatic_duty_cycle": "Automatische inschakelduur", "overshoot_protection": "Overshoot Bescherming (met PWM)", - "window_minimum_open_time": "Minimale open tijd van het raam", - "sensor_max_value_age": "Maximale leeftijd van de waarden van de temperatuursensor" + "sensor_max_value_age": "Maximale leeftijd van de waarden van de temperatuursensor", + "window_minimum_open_time": "Minimale open tijd van het raam" }, "data_description": { "automatic_duty_cycle": "Automatische inschakelduur voor Pulsbreedtemodulatie (PWM) in- of uitschakelen.", "overshoot_protection": "Overshoot Bescherming inschakelen met Pulsbreedtemodulatie (PWM) om temperatuur-overshoot van de ketel te voorkomen.", - "window_minimum_open_time": "De minimale tijd dat een raam open moet zijn voordat het systeem reageert.", - "sensor_max_value_age": "De maximale leeftijd van de waarden van de temperatuursensor voordat het als verouderd wordt beschouwd." - } - }, - "advanced": { - "title": "Geavanceerd", - "data": { - "simulation": "Simulatie", - "sample_time": "Sampletijd", - "thermal_comfort": "Thermisch Comfort", - "minimum_consumption": "Minimaal Verbruik", - "maximum_consumption": "Maximaal Verbruik", - "climate_valve_offset": "Offset van Klimaatklep", - "target_temperature_step": "Stap van Doeltemperatuur", - "maximum_relative_modulation": "Maximale Relatieve Modulatie", - "force_pulse_width_modulation": "Dwing Pulsbreedtemodulatie af", - "dynamic_minimum_setpoint": "Dynamisch Minimaal Setpoint (Experimenteel)" + "sensor_max_value_age": "De maximale leeftijd van de waarden van de temperatuursensor voordat het als verouderd wordt beschouwd.", + "window_minimum_open_time": "De minimale tijd dat een raam open moet zijn voordat het systeem reageert." }, - "data_description": { - "thermal_comfort": "Gebruik van de Simmer Index voor aanpassing van thermisch comfort inschakelen.", - "minimum_consumption": "Het minimale gasverbruik wanneer de ketel actief is.", - "maximum_consumption": "Het maximale gasverbruik wanneer de ketel actief is.", - "climate_valve_offset": "Offset om de openingsgraad van de klimaatklep aan te passen.", - "target_temperature_step": "De stap van de doeltemperatuur aanpassen voor fijnafstemming van comfortniveaus.", - "sample_time": "Het minimale tijdsinterval tussen updates aan de PID-regelaar.", - "maximum_relative_modulation": "Vertegenwoordigt het hoogste modulatieniveau voor een efficiënt verwarmingssysteem." - } + "description": "Voor fijnafstelling en aanpassing.", + "title": "Systeemconfiguratie" } } } diff --git a/custom_components/sat/util.py b/custom_components/sat/util.py index 52c40b97..f0ab210f 100644 --- a/custom_components/sat/util.py +++ b/custom_components/sat/util.py @@ -47,6 +47,7 @@ def create_pid_controller(config_options) -> PID: kd = float(config_options.get(CONF_DERIVATIVE)) heating_system = config_options.get(CONF_HEATING_SYSTEM) + version = int(config_options.get(CONF_PID_CONTROLLER_VERSION)) automatic_gains = bool(config_options.get(CONF_AUTOMATIC_GAINS)) automatic_gains_value = float(config_options.get(CONF_AUTOMATIC_GAINS_VALUE)) derivative_time_weight = float(config_options.get(CONF_DERIVATIVE_TIME_WEIGHT)) @@ -54,6 +55,7 @@ def create_pid_controller(config_options) -> PID: # Return a new PID controller instance with the given configuration options return PID( + version=version, heating_system=heating_system, automatic_gain_value=automatic_gains_value, derivative_time_weight=derivative_time_weight, From 61e884ee6826b424c4afb152f6c1b84b88364798 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 11 Feb 2024 15:46:57 +0100 Subject: [PATCH 006/213] Revert --- custom_components/sat/pid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/pid.py b/custom_components/sat/pid.py index 4e59f1b4..a6279102 100644 --- a/custom_components/sat/pid.py +++ b/custom_components/sat/pid.py @@ -351,12 +351,12 @@ def output(self) -> float: @property def integral_enabled(self) -> bool: """Return whether the updates of the integral are enabled.""" - return abs(self.previous_error) <= self._deadband + return abs(self._last_error) <= self._deadband @property def derivative_enabled(self) -> bool: """Return whether the updates of the derivative are enabled.""" - return abs(self.previous_error) > self._deadband + return abs(self._last_error) > self._deadband @property def num_errors(self) -> int: From 2e2ee18027a4d0c77ad3392b62fb7689e7980ff5 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 11 Feb 2024 19:24:02 +0100 Subject: [PATCH 007/213] Add base return temperature to the attributes to monitor --- custom_components/sat/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 3e9c1f05..6186c779 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -370,6 +370,7 @@ def extra_state_attributes(self): "minimum_setpoint": self.minimum_setpoint, "requested_setpoint": self.requested_setpoint, "adjusted_minimum_setpoint": self.adjusted_minimum_setpoint, + "base_return_temperature": self._minimum_setpoint.base_return_temperature, "outside_temperature": self.current_outside_temperature, "optimal_coefficient": self.heating_curve.optimal_coefficient, "coefficient_derivative": self.heating_curve.coefficient_derivative, From 0029926784518773a1e07c48e945079f5ef702ab Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Mon, 12 Feb 2024 13:49:38 +0100 Subject: [PATCH 008/213] Change order of resetting --- custom_components/sat/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 6186c779..af0d8308 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -985,9 +985,6 @@ async def async_set_target_temperature(self, temperature: float) -> None: # Set the new target temperature self._target_temperature = temperature - # Reset the PID controller - await self._async_control_pid(True) - # Set the target temperature for each main climate for entity_id in self._main_climates: data = {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: temperature} @@ -997,6 +994,9 @@ async def async_set_target_temperature(self, temperature: float) -> None: # Set the target temperature for the connected boiler await self._coordinator.async_set_control_thermostat_setpoint(temperature) + # Reset the PID controller + await self._async_control_pid(True) + # Write the state to Home Assistant self.async_write_ha_state() From 82b01befa776b38e049512dc182c0d574372341d Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Mon, 12 Feb 2024 14:35:18 +0100 Subject: [PATCH 009/213] Make sure we use the highest temperatures --- custom_components/sat/minimum_setpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/minimum_setpoint.py b/custom_components/sat/minimum_setpoint.py index 2ed28269..af12c5d3 100644 --- a/custom_components/sat/minimum_setpoint.py +++ b/custom_components/sat/minimum_setpoint.py @@ -33,7 +33,7 @@ def warming_up(self, return_temperature: float) -> None: # Directly calculate the 90th percentile of the recent return temperatures here if self.recent_return_temperatures: - sorted_temperatures = sorted(self.recent_return_temperatures) + sorted_temperatures = sorted(self.recent_return_temperatures, reverse=True) index = int(len(sorted_temperatures) * 0.9) - 1 self.base_return_temperature = sorted_temperatures[max(index, 0)] From 897b8cd5d4f97caafe04151a1ae2cbe3241258e3 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Mon, 12 Feb 2024 14:36:43 +0100 Subject: [PATCH 010/213] Revert again --- custom_components/sat/minimum_setpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/minimum_setpoint.py b/custom_components/sat/minimum_setpoint.py index af12c5d3..cb42c733 100644 --- a/custom_components/sat/minimum_setpoint.py +++ b/custom_components/sat/minimum_setpoint.py @@ -45,7 +45,7 @@ def calculate(self, return_temperature: float) -> None: if self.base_return_temperature is None: return - adjustment = (return_temperature - self.base_return_temperature) * self.adjustment_factor + adjustment = (self.base_return_temperature - return_temperature) * self.adjustment_factor self.current_minimum_setpoint = self.configured_minimum_setpoint + adjustment _LOGGER.debug(f"Calculated new minimum setpoint: {self.current_minimum_setpoint}") From 1ec64ae5d32b04ac27f1f632f4cda5ba2341757a Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 13 Feb 2024 17:03:53 +0100 Subject: [PATCH 011/213] Some cleaning up --- custom_components/sat/climate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index af0d8308..37afd35d 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -249,11 +249,12 @@ async def _register_event_listeners(self): if len(self._window_sensors) > 0: entities = entity_registry.async_get(self.hass) - unique_id = f"{self._config_entry.data.get(CONF_NAME).lower()}-window-sensor" + device_name = self._config_entry.data.get(CONF_NAME) + window_id = entities.async_get_entity_id(BINARY_SENSOR_DOMAIN, DOMAIN, f"{device_name.lower()}-window-sensor") self.async_on_remove( async_track_state_change_event( - self.hass, [entities.async_get_entity_id(BINARY_SENSOR_DOMAIN, DOMAIN, unique_id)], self._async_window_sensor_changed + self.hass, [window_id], self._async_window_sensor_changed ) ) From b3494a524958575a1de157bd8b600308dea70b9c Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 13 Feb 2024 17:05:17 +0100 Subject: [PATCH 012/213] Assume we are warming up even if the setpoint is different --- custom_components/sat/climate.py | 2 +- custom_components/sat/minimum_setpoint.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 37afd35d..580f8c98 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -879,7 +879,7 @@ async def async_control_heating_loop(self, _time=None) -> None: if not self._coordinator.hot_water_active and self._coordinator.flame_active: # Calculate the base return temperature - if self.warming_up and self._coordinator.setpoint >= self._coordinator.maximum_setpoint: + if self.warming_up: self._minimum_setpoint.warming_up(self._coordinator.return_temperature) # Calculate the dynamic minimum setpoint diff --git a/custom_components/sat/minimum_setpoint.py b/custom_components/sat/minimum_setpoint.py index cb42c733..2827e89e 100644 --- a/custom_components/sat/minimum_setpoint.py +++ b/custom_components/sat/minimum_setpoint.py @@ -33,7 +33,7 @@ def warming_up(self, return_temperature: float) -> None: # Directly calculate the 90th percentile of the recent return temperatures here if self.recent_return_temperatures: - sorted_temperatures = sorted(self.recent_return_temperatures, reverse=True) + sorted_temperatures = sorted(self.recent_return_temperatures) index = int(len(sorted_temperatures) * 0.9) - 1 self.base_return_temperature = sorted_temperatures[max(index, 0)] From 59e7641e8f0334c52c9f136e058f5a93f557ecfe Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 14 Feb 2024 20:44:22 +0100 Subject: [PATCH 013/213] Make sure the minimum setpoint doesn't exceed the maximum --- custom_components/sat/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 580f8c98..bfe0096c 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -560,7 +560,7 @@ def minimum_setpoint(self) -> float: if not self._dynamic_minimum_setpoint: return self._coordinator.minimum_setpoint - return self.adjusted_minimum_setpoint + return min(self.adjusted_minimum_setpoint, self._coordinator.maximum_setpoint) @property def adjusted_minimum_setpoint(self) -> float: From 65ade66e9543e8efb5db245001cace19d1bdde1a Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Thu, 15 Feb 2024 17:29:34 +0100 Subject: [PATCH 014/213] Make sure we always remember the highest base return temperature when warming up --- custom_components/sat/minimum_setpoint.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/custom_components/sat/minimum_setpoint.py b/custom_components/sat/minimum_setpoint.py index 2827e89e..b881759e 100644 --- a/custom_components/sat/minimum_setpoint.py +++ b/custom_components/sat/minimum_setpoint.py @@ -14,7 +14,6 @@ def __init__(self, adjustment_factor: float, configured_minimum_setpoint: float) self._store = None self.base_return_temperature = None self.current_minimum_setpoint = None - self.recent_return_temperatures = [] self.adjustment_factor = adjustment_factor self.configured_minimum_setpoint = configured_minimum_setpoint @@ -28,16 +27,15 @@ async def async_initialize(self, hass: HomeAssistant) -> None: _LOGGER.debug("Loaded base return temperature from storage.") def warming_up(self, return_temperature: float) -> None: - self.recent_return_temperatures.append(return_temperature) - self.recent_return_temperatures = self.recent_return_temperatures[-100:] + if self.base_return_temperature is not None and self.base_return_temperature > return_temperature: + return - # Directly calculate the 90th percentile of the recent return temperatures here - if self.recent_return_temperatures: - sorted_temperatures = sorted(self.recent_return_temperatures) - index = int(len(sorted_temperatures) * 0.9) - 1 - self.base_return_temperature = sorted_temperatures[max(index, 0)] + # Use the new value if it's higher or none is set + self.base_return_temperature = return_temperature + _LOGGER.debug(f"Higher temperature set to: {return_temperature}.") - if self._store and self.base_return_temperature is not None: + # Make sure to remember this value + if self._store: self._store.async_delay_save(self._data_to_save) _LOGGER.debug("Stored base return temperature changes.") From 41df596db98b3e598b5d0f5c750dfa8af1b6331f Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 21 Feb 2024 22:32:18 +0100 Subject: [PATCH 015/213] Reversed again... --- custom_components/sat/minimum_setpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/minimum_setpoint.py b/custom_components/sat/minimum_setpoint.py index b881759e..63f0a4af 100644 --- a/custom_components/sat/minimum_setpoint.py +++ b/custom_components/sat/minimum_setpoint.py @@ -43,7 +43,7 @@ def calculate(self, return_temperature: float) -> None: if self.base_return_temperature is None: return - adjustment = (self.base_return_temperature - return_temperature) * self.adjustment_factor + adjustment = (return_temperature - self.base_return_temperature) * self.adjustment_factor self.current_minimum_setpoint = self.configured_minimum_setpoint + adjustment _LOGGER.debug(f"Calculated new minimum setpoint: {self.current_minimum_setpoint}") From a620bd7dbf379d886f40ce5d7a866cf4057882cd Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Thu, 22 Feb 2024 17:36:42 +0100 Subject: [PATCH 016/213] Make sure we reset the integral timer when we just entered deadband --- custom_components/sat/pid.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/sat/pid.py b/custom_components/sat/pid.py index a6279102..69b6ac04 100644 --- a/custom_components/sat/pid.py +++ b/custom_components/sat/pid.py @@ -126,6 +126,10 @@ def update_integral(self, error: float, heating_curve_value: float, force: bool :param heating_curve_value: The current value of the heating curve. :param force: Boolean flag indicating whether to force an update even if the integral time limit has not been reached. """ + # Make sure we reset the time if we just entered deadband + if self.last_error > self._deadband > error: + self._last_interval_updated = monotonic() + # Make sure the integral term is enabled if not self.integral_enabled: self._integral = 0 From d5e6f4732d119ee2132eb21406b0a5b1361f5496 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 27 Feb 2024 17:49:32 +0100 Subject: [PATCH 017/213] Change the boundaries of the integral limit to the raw heating curve value --- custom_components/sat/pid.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/custom_components/sat/pid.py b/custom_components/sat/pid.py index 69b6ac04..59dc7be5 100644 --- a/custom_components/sat/pid.py +++ b/custom_components/sat/pid.py @@ -141,7 +141,6 @@ def update_integral(self, error: float, heating_curve_value: float, force: bool return current_time = monotonic() - limit = heating_curve_value / 10 time_elapsed = current_time - self._last_interval_updated # Check if the integral gain `ki` is set @@ -152,8 +151,8 @@ def update_integral(self, error: float, heating_curve_value: float, force: bool self._integral += self.ki * error * time_elapsed # Clamp the integral value within the limit - self._integral = min(self._integral, float(+limit)) - self._integral = max(self._integral, float(-limit)) + self._integral = min(self._integral, float(+heating_curve_value)) + self._integral = max(self._integral, float(-heating_curve_value)) # Record the time of the latest update self._last_interval_updated = current_time From ce3e8f9302ac481cde4fc6f35bae4e226dbd70b2 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 2 Mar 2024 12:49:22 +0100 Subject: [PATCH 018/213] Some fine-tuning on when to reset the interval last updated --- custom_components/sat/pid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/pid.py b/custom_components/sat/pid.py index 59dc7be5..30f49161 100644 --- a/custom_components/sat/pid.py +++ b/custom_components/sat/pid.py @@ -127,7 +127,7 @@ def update_integral(self, error: float, heating_curve_value: float, force: bool :param force: Boolean flag indicating whether to force an update even if the integral time limit has not been reached. """ # Make sure we reset the time if we just entered deadband - if self.last_error > self._deadband > error: + if abs(self.last_error) > self._deadband >= abs(error): self._last_interval_updated = monotonic() # Make sure the integral term is enabled From bbbfc801a8108c4b1ebc6f175fc29d25f39be558 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 2 Mar 2024 12:55:42 +0100 Subject: [PATCH 019/213] Make sure dhcp is one of the dependencies --- custom_components/sat/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/manifest.json b/custom_components/sat/manifest.json index 2f1207e5..3140a873 100644 --- a/custom_components/sat/manifest.json +++ b/custom_components/sat/manifest.json @@ -6,7 +6,8 @@ ], "config_flow": true, "dependencies": [ - "mqtt" + "mqtt", + "dhcp" ], "dhcp": [ { From 6dd94d1e4ff9e09b42ce3eecced49e952b142f98 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 2 Mar 2024 14:33:05 +0100 Subject: [PATCH 020/213] Trying some things out, and also sort the manifest.json --- custom_components/sat/manifest.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/sat/manifest.json b/custom_components/sat/manifest.json index 3140a873..458b9f28 100644 --- a/custom_components/sat/manifest.json +++ b/custom_components/sat/manifest.json @@ -1,6 +1,4 @@ { - "domain": "sat", - "name": "Smart Autotune Thermostat", "codeowners": [ "@Alexwijn" ], @@ -15,13 +13,16 @@ } ], "documentation": "https://github.com/Alexwijn/SAT", + "domain": "sat", + "import_executor": true, "iot_class": "local_push", "issue_tracker": "https://github.com/Alexwijn/SAT/issues", "mqtt": [ "OTGW/value/+" ], + "name": "Smart Autotune Thermostat", "requirements": [ "pyotgw==2.1.3" ], "version": "3.0.1" -} +} \ No newline at end of file From fbfb45e130b2a00195152a90660f2cccb0fde349 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 2 Mar 2024 14:34:37 +0100 Subject: [PATCH 021/213] Revert... --- custom_components/sat/manifest.json | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/sat/manifest.json b/custom_components/sat/manifest.json index 458b9f28..d0c3fcc5 100644 --- a/custom_components/sat/manifest.json +++ b/custom_components/sat/manifest.json @@ -14,7 +14,6 @@ ], "documentation": "https://github.com/Alexwijn/SAT", "domain": "sat", - "import_executor": true, "iot_class": "local_push", "issue_tracker": "https://github.com/Alexwijn/SAT/issues", "mqtt": [ From 6efd5c351aeb4083c33fb3129b5034e33d942ac7 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 2 Mar 2024 15:35:05 +0100 Subject: [PATCH 022/213] Revert to a semi-alphabet order --- custom_components/sat/manifest.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/manifest.json b/custom_components/sat/manifest.json index d0c3fcc5..61c54398 100644 --- a/custom_components/sat/manifest.json +++ b/custom_components/sat/manifest.json @@ -1,4 +1,6 @@ { + "domain": "sat", + "name": "Smart Autotune Thermostat", "codeowners": [ "@Alexwijn" ], @@ -13,13 +15,12 @@ } ], "documentation": "https://github.com/Alexwijn/SAT", - "domain": "sat", "iot_class": "local_push", "issue_tracker": "https://github.com/Alexwijn/SAT/issues", "mqtt": [ "OTGW/value/+" ], - "name": "Smart Autotune Thermostat", + "requirements": [ "pyotgw==2.1.3" ], From afc6c123d148bec53d80a62ed77eddfed8a0aaf7 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 2 Mar 2024 18:27:56 +0100 Subject: [PATCH 023/213] Add support for manufacturers and also send a custom command when Immergas has been detected for Relative Modulation --- custom_components/sat/binary_sensor.py | 12 +++---- custom_components/sat/coordinator.py | 18 +++++++--- custom_components/sat/entity.py | 2 +- custom_components/sat/fake/__init__.py | 4 +++ custom_components/sat/manufacturer.py | 26 ++++++++++++++ .../sat/manufacturers/ferroli.py | 7 ++++ .../sat/manufacturers/immergas.py | 7 ++++ custom_components/sat/manufacturers/nefit.py | 7 ++++ .../sat/manufacturers/simulator.py | 7 ++++ custom_components/sat/mqtt/__init__.py | 16 ++++++++- custom_components/sat/sensor.py | 36 +++++++++++-------- custom_components/sat/simulator/__init__.py | 4 +++ custom_components/sat/switch/__init__.py | 4 +++ 13 files changed, 124 insertions(+), 26 deletions(-) create mode 100644 custom_components/sat/manufacturer.py create mode 100644 custom_components/sat/manufacturers/ferroli.py create mode 100644 custom_components/sat/manufacturers/immergas.py create mode 100644 custom_components/sat/manufacturers/nefit.py create mode 100644 custom_components/sat/manufacturers/simulator.py diff --git a/custom_components/sat/binary_sensor.py b/custom_components/sat/binary_sensor.py index e17010cf..2321653f 100644 --- a/custom_components/sat/binary_sensor.py +++ b/custom_components/sat/binary_sensor.py @@ -30,15 +30,15 @@ async def async_setup_entry(_hass: HomeAssistant, _config_entry: ConfigEntry, _a await serial_binary_sensor.async_setup_entry(_hass, _config_entry, _async_add_entities) if coordinator.supports_setpoint_management: - _async_add_entities([SatControlSetpointSynchroSensor(coordinator, climate, _config_entry)]) + _async_add_entities([SatControlSetpointSynchroSensor(coordinator, _config_entry, climate)]) if coordinator.supports_relative_modulation_management: - _async_add_entities([SatRelativeModulationSynchroSensor(coordinator, climate, _config_entry)]) + _async_add_entities([SatRelativeModulationSynchroSensor(coordinator, _config_entry, climate)]) if len(_config_entry.options.get(CONF_WINDOW_SENSORS, [])) > 0: - _async_add_entities([SatWindowSensor(coordinator, climate, _config_entry)]) + _async_add_entities([SatWindowSensor(coordinator, _config_entry, climate)]) - _async_add_entities([SatCentralHeatingSynchroSensor(coordinator, climate, _config_entry)]) + _async_add_entities([SatCentralHeatingSynchroSensor(coordinator, _config_entry, climate)]) class SatControlSetpointSynchroSensor(SatClimateEntity, BinarySensorEntity): @@ -133,8 +133,8 @@ def unique_id(self) -> str: class SatWindowSensor(SatClimateEntity, BinarySensorGroup): - def __init__(self, coordinator, climate: SatClimate, config_entry: ConfigEntry): - super().__init__(coordinator, climate, config_entry) + def __init__(self, coordinator, config_entry: ConfigEntry, climate: SatClimate): + super().__init__(coordinator, config_entry, climate) self.mode = any self._entity_ids = self._config_entry.options.get(CONF_WINDOW_SENSORS) diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index a1eeb1d4..c45b1210 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -11,6 +11,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import * +from .manufacturer import ManufacturerFactory, Manufacturer from .util import calculate_default_maximum_setpoint if TYPE_CHECKING: @@ -41,14 +42,14 @@ async def resolve( from .simulator import SatSimulatorCoordinator return SatSimulatorCoordinator(hass=hass, data=data, options=options) - if mode == MODE_MQTT: - from .mqtt import SatMqttCoordinator - return SatMqttCoordinator(hass=hass, device_id=device, data=data, options=options) - if mode == MODE_SWITCH: from .switch import SatSwitchCoordinator return SatSwitchCoordinator(hass=hass, entity_id=device, data=data, options=options) + if mode == MODE_MQTT: + from .mqtt import SatMqttCoordinator + return await SatMqttCoordinator(hass=hass, device_id=device, data=data, options=options).boot() + if mode == MODE_SERIAL: from .serial import SatSerialCoordinator return await SatSerialCoordinator(hass=hass, port=device, data=data, options=options).async_connect() @@ -74,6 +75,10 @@ def device_state(self): """Return the current state of the device.""" return self._device_state + @property + def manufacturer(self) -> Manufacturer | None: + return ManufacturerFactory().resolve(self.member_id) + @property @abstractmethod def setpoint(self) -> float | None: @@ -84,6 +89,11 @@ def setpoint(self) -> float | None: def device_active(self) -> bool: pass + @property + @abstractmethod + def member_id(self) -> int: + pass + @property def flame_active(self) -> bool: return self.device_active diff --git a/custom_components/sat/entity.py b/custom_components/sat/entity.py index b4cea2f1..c166dcc4 100644 --- a/custom_components/sat/entity.py +++ b/custom_components/sat/entity.py @@ -33,7 +33,7 @@ def device_info(self): class SatClimateEntity(SatEntity): - def __init__(self, coordinator, climate: SatClimate, config_entry: ConfigEntry): + def __init__(self, coordinator, config_entry: ConfigEntry, climate: SatClimate): super().__init__(coordinator, config_entry) self._climate = climate diff --git a/custom_components/sat/fake/__init__.py b/custom_components/sat/fake/__init__.py index d95c73a5..5d96aa37 100644 --- a/custom_components/sat/fake/__init__.py +++ b/custom_components/sat/fake/__init__.py @@ -27,6 +27,10 @@ def __init__( class SatFakeCoordinator(SatDataUpdateCoordinator): """Class to manage to fetch data from the OTGW Gateway using mqtt.""" + @property + def member_id(self) -> int: + return -1 + def __init__(self, hass: HomeAssistant, data: Mapping[str, Any], options: Mapping[str, Any] | None = None) -> None: self.data = {} self.config = SatFakeConfig(True) diff --git a/custom_components/sat/manufacturer.py b/custom_components/sat/manufacturer.py new file mode 100644 index 00000000..134b98d0 --- /dev/null +++ b/custom_components/sat/manufacturer.py @@ -0,0 +1,26 @@ +from abc import abstractmethod + + +class Manufacturer: + @property + @abstractmethod + def name(self) -> str: + pass + + +class ManufacturerFactory: + @abstractmethod + def resolve(self, member_id: int) -> Manufacturer | None: + if member_id == -1: + from custom_components.sat.manufacturers.simulator import Simulator + return Simulator() + + if member_id == 27: + from custom_components.sat.manufacturers.immergas import Immergas + return Immergas() + + if member_id == 131: + from custom_components.sat.manufacturers.nefit import Nefit + return Nefit() + + return None diff --git a/custom_components/sat/manufacturers/ferroli.py b/custom_components/sat/manufacturers/ferroli.py new file mode 100644 index 00000000..a08ae1fa --- /dev/null +++ b/custom_components/sat/manufacturers/ferroli.py @@ -0,0 +1,7 @@ +from custom_components.sat.manufacturer import Manufacturer + + +class Ferroli(Manufacturer): + @property + def name(self) -> str: + return 'Ferroli' diff --git a/custom_components/sat/manufacturers/immergas.py b/custom_components/sat/manufacturers/immergas.py new file mode 100644 index 00000000..ccc0585d --- /dev/null +++ b/custom_components/sat/manufacturers/immergas.py @@ -0,0 +1,7 @@ +from custom_components.sat.manufacturer import Manufacturer + + +class Immergas(Manufacturer): + @property + def name(self) -> str: + return 'Immergas' diff --git a/custom_components/sat/manufacturers/nefit.py b/custom_components/sat/manufacturers/nefit.py new file mode 100644 index 00000000..dc33e454 --- /dev/null +++ b/custom_components/sat/manufacturers/nefit.py @@ -0,0 +1,7 @@ +from custom_components.sat.manufacturer import Manufacturer + + +class Nefit(Manufacturer): + @property + def name(self) -> str: + return 'Nefit' diff --git a/custom_components/sat/manufacturers/simulator.py b/custom_components/sat/manufacturers/simulator.py new file mode 100644 index 00000000..84a5934b --- /dev/null +++ b/custom_components/sat/manufacturers/simulator.py @@ -0,0 +1,7 @@ +from custom_components.sat.manufacturer import Manufacturer + + +class Simulator(Manufacturer): + @property + def name(self) -> str: + return 'Simulator' diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 73b812b0..90980687 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -14,6 +14,7 @@ from ..const import * from ..coordinator import DeviceState, SatDataUpdateCoordinator +from ..manufacturers.immergas import Immergas DATA_FLAME_ACTIVE = "flame" DATA_DHW_SETPOINT = "TdhwSet" @@ -23,6 +24,7 @@ DATA_RETURN_TEMPERATURE = "Tret" DATA_DHW_ENABLE = "domestichotwater" DATA_CENTRAL_HEATING = "centralheating" +DATA_SLAVE_MEMBER_ID = "slave_memberid_code" DATA_BOILER_CAPACITY = "MaxCapacityMinModLevel_hb_u8" DATA_REL_MIN_MOD_LEVEL = "MaxCapacityMinModLevel_lb_u8" DATA_REL_MIN_MOD_LEVELL = "MaxCapacityMinModLevell_lb_u8" @@ -152,6 +154,16 @@ def maximum_relative_modulation_value(self) -> float | None: return super().maximum_relative_modulation_value + @property + def member_id(self) -> int: + return int(self._get_entity_state(SENSOR_DOMAIN, DATA_SLAVE_MEMBER_ID)) + + async def boot(self) -> SatMqttCoordinator: + await self._send_command("PM=3") + await self._send_command("PM=48") + + return self + async def async_added_to_hass(self, climate: SatClimate) -> None: await mqtt.async_wait_for_mqtt_client(self.hass) @@ -176,7 +188,6 @@ async def async_added_to_hass(self, climate: SatClimate) -> None: # Track those entities so the coordinator can be updated when something changes async_track_state_change_event(self.hass, entities, self.async_state_change_event) - await self._send_command("PM=48") await super().async_added_to_hass(climate) async def async_state_change_event(self, event: Event): @@ -206,6 +217,9 @@ async def async_set_heater_state(self, state: DeviceState) -> None: await super().async_set_heater_state(state) async def async_set_control_max_relative_modulation(self, value: int) -> None: + if isinstance(self.manufacturer, Immergas): + await self._send_command(f"TP=11:12={value}") + await self._send_command(f"MM={value}") await super().async_set_control_max_relative_modulation(value) diff --git a/custom_components/sat/sensor.py b/custom_components/sat/sensor.py index cc0536c1..8b1b9ce7 100644 --- a/custom_components/sat/sensor.py +++ b/custom_components/sat/sensor.py @@ -12,12 +12,12 @@ from .const import CONF_MODE, MODE_SERIAL, CONF_NAME, DOMAIN, COORDINATOR, CLIMATE, MODE_SIMULATOR, CONF_MINIMUM_CONSUMPTION, CONF_MAXIMUM_CONSUMPTION from .coordinator import SatDataUpdateCoordinator -from .entity import SatEntity +from .entity import SatEntity, SatClimateEntity from .serial import sensor as serial_sensor from .simulator import sensor as simulator_sensor if typing.TYPE_CHECKING: - from .climate import SatClimate + pass _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -38,6 +38,7 @@ async def async_setup_entry(_hass: HomeAssistant, _config_entry: ConfigEntry, _a await simulator_sensor.async_setup_entry(_hass, _config_entry, _async_add_entities) _async_add_entities([ + SatManufacturerSensor(coordinator, _config_entry), SatErrorValueSensor(coordinator, _config_entry, climate), SatHeatingCurveSensor(coordinator, _config_entry, climate), ]) @@ -135,12 +136,7 @@ def unique_id(self) -> str: return f"{self._config_entry.data.get(CONF_NAME).lower()}-boiler-current-consumption" -class SatHeatingCurveSensor(SatEntity, SensorEntity): - - def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEntry, climate: SatClimate): - super().__init__(coordinator, config_entry) - - self._climate = climate +class SatHeatingCurveSensor(SatClimateEntity, SensorEntity): async def async_added_to_hass(self) -> None: async def on_state_change(_event: Event): @@ -185,12 +181,7 @@ def unique_id(self) -> str: return f"{self._config_entry.data.get(CONF_NAME).lower()}-heating-curve" -class SatErrorValueSensor(SatEntity, SensorEntity): - - def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEntry, climate: SatClimate): - super().__init__(coordinator, config_entry) - - self._climate = climate +class SatErrorValueSensor(SatClimateEntity, SensorEntity): async def async_added_to_hass(self) -> None: async def on_state_change(_event: Event): @@ -233,3 +224,20 @@ def native_value(self) -> float: def unique_id(self) -> str: """Return a unique ID to use for this entity.""" return f"{self._config_entry.data.get(CONF_NAME).lower()}-error-value" + + +class SatManufacturerSensor(SatEntity, SensorEntity): + @property + def name(self) -> str: + return f"Boiler Manufacturer" + + @property + def native_value(self) -> str: + if not (manufacturer := self._coordinator.manufacturer): + return "Unknown" + + return manufacturer.name + + @property + def unique_id(self) -> str: + return f"{self._config_entry.data.get(CONF_NAME).lower()}-manufacturer" diff --git a/custom_components/sat/simulator/__init__.py b/custom_components/sat/simulator/__init__.py index 62b0d046..883c5ece 100644 --- a/custom_components/sat/simulator/__init__.py +++ b/custom_components/sat/simulator/__init__.py @@ -61,6 +61,10 @@ def flame_active(self) -> bool: def relative_modulation_value(self) -> float | None: return 100 if self.flame_active else 0 + @property + def member_id(self) -> int: + return -1 + async def async_set_heater_state(self, state: DeviceState) -> None: self._started_on = monotonic() if state == DeviceState.ON else None diff --git a/custom_components/sat/switch/__init__.py b/custom_components/sat/switch/__init__.py index 4412f836..04ecc817 100644 --- a/custom_components/sat/switch/__init__.py +++ b/custom_components/sat/switch/__init__.py @@ -40,6 +40,10 @@ def device_active(self) -> bool: return state.state == STATE_ON + @property + def member_id(self) -> int: + return -1 + async def async_set_heater_state(self, state: DeviceState) -> None: if not self._simulation: domain_service = DOMAIN_SERVICE.get(self._entity.domain) From 4ffaf826ed5473954ed00e4ec99e08f60baab6fd Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 2 Mar 2024 18:31:59 +0100 Subject: [PATCH 024/213] Make sure we wait for the member id --- custom_components/sat/coordinator.py | 5 ++++- custom_components/sat/fake/__init__.py | 2 +- custom_components/sat/mqtt/__init__.py | 9 ++++++--- custom_components/sat/simulator/__init__.py | 2 +- custom_components/sat/switch/__init__.py | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index c45b1210..dda4c110 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -77,6 +77,9 @@ def device_state(self): @property def manufacturer(self) -> Manufacturer | None: + if self.member_id is None: + return None + return ManufacturerFactory().resolve(self.member_id) @property @@ -91,7 +94,7 @@ def device_active(self) -> bool: @property @abstractmethod - def member_id(self) -> int: + def member_id(self) -> int | None: pass @property diff --git a/custom_components/sat/fake/__init__.py b/custom_components/sat/fake/__init__.py index 5d96aa37..316ddfef 100644 --- a/custom_components/sat/fake/__init__.py +++ b/custom_components/sat/fake/__init__.py @@ -28,7 +28,7 @@ class SatFakeCoordinator(SatDataUpdateCoordinator): """Class to manage to fetch data from the OTGW Gateway using mqtt.""" @property - def member_id(self) -> int: + def member_id(self) -> int | None: return -1 def __init__(self, hass: HomeAssistant, data: Mapping[str, Any], options: Mapping[str, Any] | None = None) -> None: diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 90980687..6afd4a09 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -155,8 +155,11 @@ def maximum_relative_modulation_value(self) -> float | None: return super().maximum_relative_modulation_value @property - def member_id(self) -> int: - return int(self._get_entity_state(SENSOR_DOMAIN, DATA_SLAVE_MEMBER_ID)) + def member_id(self) -> int | None: + if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_SLAVE_MEMBER_ID)) is not None: + return int(value) + + return None async def boot(self) -> SatMqttCoordinator: await self._send_command("PM=3") @@ -218,7 +221,7 @@ async def async_set_heater_state(self, state: DeviceState) -> None: async def async_set_control_max_relative_modulation(self, value: int) -> None: if isinstance(self.manufacturer, Immergas): - await self._send_command(f"TP=11:12={value}") + await self._send_command(f"TP=11:12={min(value, 80)}") await self._send_command(f"MM={value}") diff --git a/custom_components/sat/simulator/__init__.py b/custom_components/sat/simulator/__init__.py index 883c5ece..eb579242 100644 --- a/custom_components/sat/simulator/__init__.py +++ b/custom_components/sat/simulator/__init__.py @@ -62,7 +62,7 @@ def relative_modulation_value(self) -> float | None: return 100 if self.flame_active else 0 @property - def member_id(self) -> int: + def member_id(self) -> int | None: return -1 async def async_set_heater_state(self, state: DeviceState) -> None: diff --git a/custom_components/sat/switch/__init__.py b/custom_components/sat/switch/__init__.py index 04ecc817..a31fb7de 100644 --- a/custom_components/sat/switch/__init__.py +++ b/custom_components/sat/switch/__init__.py @@ -41,7 +41,7 @@ def device_active(self) -> bool: return state.state == STATE_ON @property - def member_id(self) -> int: + def member_id(self) -> int | None: return -1 async def async_set_heater_state(self, state: DeviceState) -> None: From 69e8581cba7cb48dcd8042a3068d2cf26724d9bc Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 2 Mar 2024 18:44:57 +0100 Subject: [PATCH 025/213] Add some caching --- custom_components/sat/coordinator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index dda4c110..ce72b8a1 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -64,6 +64,7 @@ def __init__(self, hass: HomeAssistant, data: Mapping[str, Any], options: Mappin self._data = data self._options = options + self._manufacturer = None self._device_state = DeviceState.OFF self._simulation = bool(data.get(CONF_SIMULATION)) self._heating_system = str(data.get(CONF_HEATING_SYSTEM, HEATING_SYSTEM_UNKNOWN)) @@ -80,7 +81,10 @@ def manufacturer(self) -> Manufacturer | None: if self.member_id is None: return None - return ManufacturerFactory().resolve(self.member_id) + if self._manufacturer is None: + self._manufacturer = ManufacturerFactory().resolve(self.member_id) + + return self._manufacturer @property @abstractmethod From 0c5d3a9395d73e97b5ab90b1250ab32eb954cc64 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 3 Mar 2024 14:53:43 +0100 Subject: [PATCH 026/213] Add more manufacturers --- custom_components/sat/manufacturer.py | 8 ++++++++ custom_components/sat/manufacturers/dedietrich.py | 7 +++++++ custom_components/sat/manufacturers/ideal.py | 7 +++++++ 3 files changed, 22 insertions(+) create mode 100644 custom_components/sat/manufacturers/dedietrich.py create mode 100644 custom_components/sat/manufacturers/ideal.py diff --git a/custom_components/sat/manufacturer.py b/custom_components/sat/manufacturer.py index 134b98d0..65cfa932 100644 --- a/custom_components/sat/manufacturer.py +++ b/custom_components/sat/manufacturer.py @@ -15,6 +15,14 @@ def resolve(self, member_id: int) -> Manufacturer | None: from custom_components.sat.manufacturers.simulator import Simulator return Simulator() + if member_id == 6: + from custom_components.sat.manufacturers.ideal import Ideal + return Ideal() + + if member_id == 11: + from custom_components.sat.manufacturers.dedietrich import DeDietrich + return DeDietrich() + if member_id == 27: from custom_components.sat.manufacturers.immergas import Immergas return Immergas() diff --git a/custom_components/sat/manufacturers/dedietrich.py b/custom_components/sat/manufacturers/dedietrich.py new file mode 100644 index 00000000..b3f1bda7 --- /dev/null +++ b/custom_components/sat/manufacturers/dedietrich.py @@ -0,0 +1,7 @@ +from custom_components.sat.manufacturer import Manufacturer + + +class DeDietrich(Manufacturer): + @property + def name(self) -> str: + return 'De Dietrich' diff --git a/custom_components/sat/manufacturers/ideal.py b/custom_components/sat/manufacturers/ideal.py new file mode 100644 index 00000000..cb0aa6e2 --- /dev/null +++ b/custom_components/sat/manufacturers/ideal.py @@ -0,0 +1,7 @@ +from custom_components.sat.manufacturer import Manufacturer + + +class Ideal(Manufacturer): + @property + def name(self) -> str: + return 'Ideal' From 15473af9b993d2fcde0a6dc225bcdf90f3f5efe3 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 3 Mar 2024 15:32:29 +0100 Subject: [PATCH 027/213] Add Intergas --- custom_components/sat/manufacturer.py | 4 ++++ custom_components/sat/manufacturers/intergas.py | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 custom_components/sat/manufacturers/intergas.py diff --git a/custom_components/sat/manufacturer.py b/custom_components/sat/manufacturer.py index 65cfa932..85dc76a8 100644 --- a/custom_components/sat/manufacturer.py +++ b/custom_components/sat/manufacturer.py @@ -31,4 +31,8 @@ def resolve(self, member_id: int) -> Manufacturer | None: from custom_components.sat.manufacturers.nefit import Nefit return Nefit() + if member_id == 173: + from custom_components.sat.manufacturers.intergas import Intergas + return Intergas() + return None diff --git a/custom_components/sat/manufacturers/intergas.py b/custom_components/sat/manufacturers/intergas.py new file mode 100644 index 00000000..38ba8825 --- /dev/null +++ b/custom_components/sat/manufacturers/intergas.py @@ -0,0 +1,7 @@ +from custom_components.sat.manufacturer import Manufacturer + + +class Intergas(Manufacturer): + @property + def name(self) -> str: + return 'Intergas' From 49bff2834cd9f51683e1373e135fa44ab0c41e6d Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 3 Mar 2024 15:36:21 +0100 Subject: [PATCH 028/213] Add missing Ferroli import --- custom_components/sat/manufacturer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/sat/manufacturer.py b/custom_components/sat/manufacturer.py index 85dc76a8..fbf5a627 100644 --- a/custom_components/sat/manufacturer.py +++ b/custom_components/sat/manufacturer.py @@ -19,6 +19,10 @@ def resolve(self, member_id: int) -> Manufacturer | None: from custom_components.sat.manufacturers.ideal import Ideal return Ideal() + if member_id == 9: + from custom_components.sat.manufacturers.ferroli import Ferroli + return Ferroli() + if member_id == 11: from custom_components.sat.manufacturers.dedietrich import DeDietrich return DeDietrich() From 8942a8f93d086483c9163b9898f5bf246df60a3a Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 17 Mar 2024 17:13:56 +0100 Subject: [PATCH 029/213] Giving the virtual sensors some unique ids for testing --- configuration.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/configuration.yaml b/configuration.yaml index 1709f622..e63ee222 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -4,6 +4,7 @@ logger: default: info logs: custom_components.sat: debug + custom_components.interpolated_sensor: debug homeassistant: customize: @@ -39,18 +40,22 @@ template: - unit_of_measurement: °C name: Heater Temperature device_class: 'temperature' + unique_id: heater_temperature state: "{{ states('input_number.heater_temperature_raw') }}" - unit_of_measurement: °C name: Current Temperature device_class: 'temperature' + unique_id: current_temperature state: "{{ states('input_number.current_temperature_raw') }}" - unit_of_measurement: °C name: Outside Temperature device_class: 'temperature' + unique_id: outside_temperature state: "{{ states('input_number.outside_temperature_raw') }}" - unit_of_measurement: "%" name: Current Humidity device_class: 'humidity' + unique_id: current_humidity state: "{{ states('input_number.humidity_raw') }}" input_number: From 96015cd8fbfff362e02301a2aba0098ccdd4a906 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 17 Mar 2024 17:14:15 +0100 Subject: [PATCH 030/213] Fixed the window sensor handler --- custom_components/sat/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index bfe0096c..f5afbbc9 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -146,7 +146,7 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn self._climates = config_entry.data.get(CONF_SECONDARY_CLIMATES) or [] self._main_climates = config_entry.data.get(CONF_MAIN_CLIMATES) or [] - self._window_sensors = config_entry.data.get(CONF_WINDOW_SENSORS) or [] + self._window_sensors = config_entry.options.get(CONF_WINDOW_SENSORS) or [] self._simulation = bool(config_entry.data.get(CONF_SIMULATION)) self._heating_system = str(config_entry.data.get(CONF_HEATING_SYSTEM)) From 4453a337662d34ccff1da69cc33eed2405a300ff Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Mar 2024 14:34:20 +0100 Subject: [PATCH 031/213] Add support for reconfiguring the entity --- custom_components/sat/config_flow.py | 86 +++++++++++++--------- custom_components/sat/translations/de.json | 3 +- custom_components/sat/translations/en.json | 3 +- custom_components/sat/translations/es.json | 3 +- custom_components/sat/translations/fr.json | 3 +- custom_components/sat/translations/it.json | 3 +- custom_components/sat/translations/nl.json | 3 +- 7 files changed, 65 insertions(+), 39 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 8ec98310..e14b8f9f 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -1,6 +1,7 @@ """Adds config flow for SAT.""" import asyncio import logging +from typing import Any import voluptuous as vol from homeassistant import config_entries @@ -45,6 +46,7 @@ def __init__(self): """Initialize.""" self.data = {} self.errors = {} + self.config_entry = None @staticmethod @callback @@ -56,7 +58,7 @@ def async_remove(self) -> None: if self.calibration is not None: self.calibration.cancel() - async def async_step_user(self, _user_input=None) -> FlowResult: + async def async_step_user(self, _user_input: dict[str, Any] | None = None) -> FlowResult: """Handle user flow.""" menu_options = [] @@ -100,7 +102,7 @@ async def async_step_mqtt(self, discovery_info: MqttServiceInfo): return await self.async_step_mosquitto() - async def async_step_mosquitto(self, _user_input=None): + async def async_step_mosquitto(self, _user_input: dict[str, Any] | None = None): self.errors = {} if _user_input is not None: @@ -126,7 +128,7 @@ async def async_step_mosquitto(self, _user_input=None): }), ) - async def async_step_serial(self, _user_input=None): + async def async_step_serial(self, _user_input: dict[str, Any] | None = None): self.errors = {} if _user_input is not None: @@ -151,7 +153,7 @@ async def async_step_serial(self, _user_input=None): }), ) - async def async_step_switch(self, _user_input=None): + async def async_step_switch(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: self.data.update(_user_input) self.data[CONF_MODE] = MODE_SWITCH @@ -173,7 +175,7 @@ async def async_step_switch(self, _user_input=None): }), ) - async def async_step_simulator(self, _user_input=None): + async def async_step_simulator(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: self.data.update(_user_input) self.data[CONF_MODE] = MODE_SIMULATOR @@ -200,9 +202,16 @@ async def async_step_simulator(self, _user_input=None): }), ) - async def async_step_sensors(self, _user_input=None): - await self.async_set_unique_id(self.data[CONF_DEVICE], raise_on_progress=False) - self._abort_if_unique_id_configured() + async def async_step_reconfigure(self, _user_input: dict[str, Any] | None = None): + self.config_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + self.data = self.config_entry.data.copy() + + return await self.async_step_sensors() + + async def async_step_sensors(self, _user_input: dict[str, Any] | None = None): + if self.config_entry is None: + await self.async_set_unique_id(self.data[CONF_DEVICE], raise_on_progress=False) + self._abort_if_unique_id_configured() if _user_input is not None: self.data.update(_user_input) @@ -216,19 +225,19 @@ async def async_step_sensors(self, _user_input=None): last_step=False, step_id="sensors", data_schema=vol.Schema({ - vol.Required(CONF_INSIDE_SENSOR_ENTITY_ID): selector.EntitySelector( + vol.Required(CONF_INSIDE_SENSOR_ENTITY_ID, default=self.data.get(CONF_INSIDE_SENSOR_ENTITY_ID)): selector.EntitySelector( selector.EntitySelectorConfig( domain=SENSOR_DOMAIN, device_class=[SensorDeviceClass.TEMPERATURE] ) ), - vol.Required(CONF_OUTSIDE_SENSOR_ENTITY_ID): selector.EntitySelector( + vol.Required(CONF_OUTSIDE_SENSOR_ENTITY_ID, default=self.data.get(CONF_OUTSIDE_SENSOR_ENTITY_ID)): selector.EntitySelector( selector.EntitySelectorConfig( multiple=True, domain=[SENSOR_DOMAIN, WEATHER_DOMAIN] ) ), - vol.Optional(CONF_HUMIDITY_SENSOR_ENTITY_ID): selector.EntitySelector( + vol.Optional(CONF_HUMIDITY_SENSOR_ENTITY_ID, default=self.data.get(CONF_HUMIDITY_SENSOR_ENTITY_ID)): selector.EntitySelector( selector.EntitySelectorConfig( domain=SENSOR_DOMAIN, device_class=[SensorDeviceClass.HUMIDITY] @@ -237,7 +246,7 @@ async def async_step_sensors(self, _user_input=None): }), ) - async def async_step_heating_system(self, _user_input=None): + async def async_step_heating_system(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: self.data.update(_user_input) @@ -247,7 +256,7 @@ async def async_step_heating_system(self, _user_input=None): last_step=False, step_id="heating_system", data_schema=vol.Schema({ - vol.Required(CONF_HEATING_SYSTEM, default=OPTIONS_DEFAULTS[CONF_HEATING_SYSTEM]): selector.SelectSelector( + vol.Required(CONF_HEATING_SYSTEM, default=self.data.get(CONF_HEATING_SYSTEM, OPTIONS_DEFAULTS[CONF_HEATING_SYSTEM])): selector.SelectSelector( selector.SelectSelectorConfig(options=[ {"value": HEATING_SYSTEM_RADIATORS, "label": "Radiators"}, {"value": HEATING_SYSTEM_HEAT_PUMP, "label": "Heat Pump"}, @@ -257,7 +266,7 @@ async def async_step_heating_system(self, _user_input=None): }) ) - async def async_step_areas(self, _user_input=None): + async def async_step_areas(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: self.data.update(_user_input) @@ -273,12 +282,12 @@ async def async_step_areas(self, _user_input=None): return self.async_show_form( step_id="areas", data_schema=vol.Schema({ - vol.Optional(CONF_MAIN_CLIMATES): climate_selector, - vol.Optional(CONF_SECONDARY_CLIMATES): climate_selector, + vol.Optional(CONF_MAIN_CLIMATES, default=self.data.get(CONF_MAIN_CLIMATES, [])): climate_selector, + vol.Optional(CONF_SECONDARY_CLIMATES, default=self.data.get(CONF_SECONDARY_CLIMATES, [])): climate_selector, }) ) - async def async_step_automatic_gains(self, _user_input=None): + async def async_step_automatic_gains(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: self.data.update(_user_input) @@ -293,13 +302,13 @@ async def async_step_automatic_gains(self, _user_input=None): data_schema=vol.Schema({vol.Required(CONF_AUTOMATIC_GAINS, default=True): bool}) ) - async def async_step_calibrate_system(self, _user_input=None): + async def async_step_calibrate_system(self, _user_input: dict[str, Any] | None = None): return self.async_show_menu( step_id="calibrate_system", menu_options=["calibrate", "overshoot_protection", "pid_controller"] ) - async def async_step_calibrate(self, _user_input=None): + async def async_step_calibrate(self, _user_input: dict[str, Any] | None = None): coordinator = await self.async_create_coordinator() async def start_calibration(): @@ -341,14 +350,14 @@ async def start_calibration(): return self.async_show_progress_done(next_step_id="calibrated") - async def async_step_calibrated(self, _user_input=None): + async def async_step_calibrated(self, _user_input: dict[str, Any] | None = None): return self.async_show_menu( step_id="calibrated", description_placeholders=self.data, menu_options=["calibrate", "finish"], ) - async def async_step_overshoot_protection(self, _user_input=None): + async def async_step_overshoot_protection(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: await self._enable_overshoot_protection( _user_input[CONF_MINIMUM_SETPOINT] @@ -359,13 +368,13 @@ async def async_step_overshoot_protection(self, _user_input=None): return self.async_show_form( step_id="overshoot_protection", data_schema=vol.Schema({ - vol.Required(CONF_MINIMUM_SETPOINT, default=OPTIONS_DEFAULTS[CONF_MINIMUM_SETPOINT]): selector.NumberSelector( + vol.Required(CONF_MINIMUM_SETPOINT, default=self.data.get(CONF_MINIMUM_SETPOINT, OPTIONS_DEFAULTS[CONF_MINIMUM_SETPOINT])): selector.NumberSelector( selector.NumberSelectorConfig(min=MINIMUM_SETPOINT, max=OVERSHOOT_PROTECTION_SETPOINT, step=1, unit_of_measurement="°C") ), }) ) - async def async_step_pid_controller(self, _user_input=None): + async def async_step_pid_controller(self, _user_input: dict[str, Any] | None = None): self.data[CONF_AUTOMATIC_GAINS] = False if _user_input is not None: @@ -375,14 +384,25 @@ async def async_step_pid_controller(self, _user_input=None): return self.async_show_form( step_id="pid_controller", data_schema=vol.Schema({ - vol.Required(CONF_PROPORTIONAL, default=OPTIONS_DEFAULTS[CONF_PROPORTIONAL]): str, - vol.Required(CONF_INTEGRAL, default=OPTIONS_DEFAULTS[CONF_INTEGRAL]): str, - vol.Required(CONF_DERIVATIVE, default=OPTIONS_DEFAULTS[CONF_DERIVATIVE]): str + vol.Required(CONF_PROPORTIONAL, default=self.data.get(CONF_PROPORTIONAL, OPTIONS_DEFAULTS[CONF_PROPORTIONAL])): str, + vol.Required(CONF_INTEGRAL, default=self.data.get(CONF_INTEGRAL, OPTIONS_DEFAULTS[CONF_INTEGRAL])): str, + vol.Required(CONF_DERIVATIVE, default=self.data.get(CONF_DERIVATIVE, OPTIONS_DEFAULTS[CONF_DERIVATIVE])): str }) ) - async def async_step_finish(self, _user_input=None): - return self.async_create_entry(title=self.data[CONF_NAME], data=self.data) + async def async_step_finish(self, _user_input: dict[str, Any] | None = None): + if self.config_entry is not None: + return self.async_update_reload_and_abort( + data=self.data, + entry=self.config_entry, + title=self.data[CONF_NAME], + reason="reconfigure_successful", + ) + + return self.async_create_entry( + title=self.data[CONF_NAME], + data=self.data + ) async def async_create_coordinator(self) -> SatDataUpdateCoordinator: # Resolve the coordinator by using the factory according to the mode @@ -402,7 +422,7 @@ def __init__(self, config_entry: ConfigEntry): self._config_entry = config_entry self._options = dict(config_entry.options) - async def async_step_init(self, _user_input=None): + async def async_step_init(self, _user_input: dict[str, Any] | None = None): menu_options = ["general", "presets", "system_configuration"] if self.show_advanced_options: @@ -413,7 +433,7 @@ async def async_step_init(self, _user_input=None): menu_options=menu_options ) - async def async_step_general(self, _user_input=None) -> FlowResult: + async def async_step_general(self, _user_input: dict[str, Any] | None = None) -> FlowResult: if _user_input is not None: return await self.update_options(_user_input) @@ -481,7 +501,7 @@ async def async_step_general(self, _user_input=None) -> FlowResult: return self.async_show_form(step_id="general", data_schema=vol.Schema(schema)) - async def async_step_presets(self, _user_input=None) -> FlowResult: + async def async_step_presets(self, _user_input: dict[str, Any] | None = None) -> FlowResult: if _user_input is not None: return await self.update_options(_user_input) @@ -508,7 +528,7 @@ async def async_step_presets(self, _user_input=None) -> FlowResult: }) ) - async def async_step_system_configuration(self, _user_input=None) -> FlowResult: + async def async_step_system_configuration(self, _user_input: dict[str, Any] | None = None) -> FlowResult: if _user_input is not None: return await self.update_options(_user_input) @@ -523,7 +543,7 @@ async def async_step_system_configuration(self, _user_input=None) -> FlowResult: }) ) - async def async_step_advanced(self, _user_input=None) -> FlowResult: + async def async_step_advanced(self, _user_input: dict[str, Any] | None = None) -> FlowResult: if _user_input is not None: return await self.update_options(_user_input) diff --git a/custom_components/sat/translations/de.json b/custom_components/sat/translations/de.json index e62ae8ef..db4050f9 100644 --- a/custom_components/sat/translations/de.json +++ b/custom_components/sat/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Gateway ist bereits konfiguriert." + "already_configured": "Gateway ist bereits konfiguriert.", + "reconfigure_successful": "Gateway wurde neu konfiguriert." }, "error": { "connection": "Verbindung zum Gateway konnte nicht hergestellt werden.", diff --git a/custom_components/sat/translations/en.json b/custom_components/sat/translations/en.json index 54b20138..aad9fafe 100644 --- a/custom_components/sat/translations/en.json +++ b/custom_components/sat/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Gateway is already configured." + "already_configured": "Gateway is already configured.", + "reconfigure_successful": "Gateway has been re-configured." }, "error": { "connection": "Unable to connect to the gateway.", diff --git a/custom_components/sat/translations/es.json b/custom_components/sat/translations/es.json index 21d4ec97..0ed9ed17 100644 --- a/custom_components/sat/translations/es.json +++ b/custom_components/sat/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La puerta de enlace ya está configurada." + "already_configured": "La puerta de enlace ya está configurada.", + "reconfigure_successful": "La puerta de enlace ha sido reconfigurada." }, "error": { "connection": "No se puede conectar a la puerta de enlace.", diff --git a/custom_components/sat/translations/fr.json b/custom_components/sat/translations/fr.json index 011235d7..d787ab8c 100644 --- a/custom_components/sat/translations/fr.json +++ b/custom_components/sat/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La passerelle est déjà configurée." + "already_configured": "La passerelle est déjà configurée.", + "reconfigure_successful": "La passerelle a été reconfigurée." }, "error": { "connection": "Impossible de se connecter à la passerelle.", diff --git a/custom_components/sat/translations/it.json b/custom_components/sat/translations/it.json index 3f51f53f..0188972e 100644 --- a/custom_components/sat/translations/it.json +++ b/custom_components/sat/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il gateway è già configurato." + "already_configured": "Il gateway è già configurato.", + "reconfigure_successful": "Il gateway è stato riconfigurato." }, "error": { "connection": "Impossibile connettersi al gateway.", diff --git a/custom_components/sat/translations/nl.json b/custom_components/sat/translations/nl.json index e51f997c..eb8b1bf9 100644 --- a/custom_components/sat/translations/nl.json +++ b/custom_components/sat/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Gateway is al geconfigureerd." + "already_configured": "Gateway is al geconfigureerd.", + "reconfigure_successful": "Gateway is opnieuw geconfigureerd." }, "error": { "connection": "Kan geen verbinding maken met de gateway.", From 21f5dd91655a52fe217c079b807d6df2a3e9d357 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Mar 2024 14:34:36 +0100 Subject: [PATCH 032/213] Fixed some deprecation warnings --- custom_components/sat/__init__.py | 4 ++-- custom_components/sat/climate.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index 37bdf4b1..6d73c92e 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -34,8 +34,8 @@ async def async_setup_entry(_hass: HomeAssistant, _entry: ConfigEntry): ) # Forward entry setup for climate and other platforms - await _hass.async_add_job(_hass.config_entries.async_forward_entry_setup(_entry, CLIMATE_DOMAIN)) - await _hass.async_add_job(_hass.config_entries.async_forward_entry_setups(_entry, [SENSOR_DOMAIN, NUMBER_DOMAIN, BINARY_SENSOR_DOMAIN])) + await _entry.async_create_task(_hass, _hass.config_entries.async_forward_entry_setup(_entry, CLIMATE_DOMAIN)) + await _entry.async_create_task(_hass, _hass.config_entries.async_forward_entry_setups(_entry, [SENSOR_DOMAIN, NUMBER_DOMAIN, BINARY_SENSOR_DOMAIN])) # Add an update listener for this entry _entry.async_on_unload(_entry.add_update_listener(async_reload_entry)) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index f5afbbc9..d2499cf9 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -80,6 +80,8 @@ def elapsed(self): class SatClimate(SatEntity, ClimateEntity, RestoreEntity): + _enable_turn_on_off_backwards_compatibility = False + def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEntry, unit: str): super().__init__(coordinator, config_entry) @@ -138,7 +140,7 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn self._attr_preset_mode = PRESET_NONE self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] self._attr_preset_modes = [PRESET_NONE] + list(self._presets.keys()) - self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TURN_OFF # System Configuration self._attr_name = str(config_entry.data.get(CONF_NAME)) From 35d6bd169c1b84ac859003898fe6b6c8ed5e71d2 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Mon, 8 Apr 2024 15:53:51 +0200 Subject: [PATCH 033/213] Attempt to check for relative modulation value to see if it accepted the MM command --- custom_components/sat/overshoot_protection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index a1ab1238..9875a9d8 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -19,13 +19,15 @@ async def calculate(self) -> float | None: _LOGGER.info("Starting calculation") await self._coordinator.async_set_heater_state(DeviceState.ON) + await self._coordinator.async_set_control_setpoint(OVERSHOOT_PROTECTION_SETPOINT) + await self._coordinator.async_set_control_max_relative_modulation(MINIMUM_RELATIVE_MOD) try: # First wait for a flame await asyncio.wait_for(self._wait_for_flame(), timeout=OVERSHOOT_PROTECTION_INITIAL_WAIT) # Since the coordinator doesn't support modulation management, so we need to fall back to find it with modulation - if not self._coordinator.supports_relative_modulation_management: + if not self._coordinator.supports_relative_modulation_management or self._coordinator.relative_modulation_value > 0: return await self._calculate_with_no_modulation_management() # Run with maximum power of the boiler, zero modulation. From 7fa32c1aee72699c0478d153b46b1baf50e7f6dc Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Mon, 8 Apr 2024 19:52:07 +0200 Subject: [PATCH 034/213] Make sure the humidity sensor falls back to UNDEFINED as this is the default, this prevents it from being required --- custom_components/sat/config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index e14b8f9f..b144c842 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -22,6 +22,7 @@ from homeassistant.helpers.selector import SelectSelectorMode from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from pyotgw import OpenThermGateway +from voluptuous import UNDEFINED from . import SatDataUpdateCoordinatorFactory from .const import * @@ -237,7 +238,7 @@ async def async_step_sensors(self, _user_input: dict[str, Any] | None = None): domain=[SENSOR_DOMAIN, WEATHER_DOMAIN] ) ), - vol.Optional(CONF_HUMIDITY_SENSOR_ENTITY_ID, default=self.data.get(CONF_HUMIDITY_SENSOR_ENTITY_ID)): selector.EntitySelector( + vol.Optional(CONF_HUMIDITY_SENSOR_ENTITY_ID, default=self.data.get(CONF_HUMIDITY_SENSOR_ENTITY_ID, UNDEFINED)): selector.EntitySelector( selector.EntitySelectorConfig( domain=SENSOR_DOMAIN, device_class=[SensorDeviceClass.HUMIDITY] From f9b1680f2300fb79550c735ea83b88b4f8b0e59f Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Mon, 8 Apr 2024 20:26:44 +0200 Subject: [PATCH 035/213] Fix some backwards compatibility issues with the TURN_OFF feature --- custom_components/sat/climate.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index d2499cf9..493126f9 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -140,7 +140,13 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn self._attr_preset_mode = PRESET_NONE self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] self._attr_preset_modes = [PRESET_NONE] + list(self._presets.keys()) - self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TURN_OFF + + # Add features based on compatibility + self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + + # Conditionally add TURN_OFF if it exists + if hasattr(ClimateEntityFeature, 'TURN_OFF'): + self._attr_supported_features |= ClimateEntityFeature.TURN_OFF # System Configuration self._attr_name = str(config_entry.data.get(CONF_NAME)) From 67adb81b966b83e5baeee60fc6ac6d16ab9354b2 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 10 Apr 2024 14:24:46 +0200 Subject: [PATCH 036/213] Wait a bit before proceeding --- custom_components/sat/overshoot_protection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 9875a9d8..60ee1ff3 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -26,6 +26,9 @@ async def calculate(self) -> float | None: # First wait for a flame await asyncio.wait_for(self._wait_for_flame(), timeout=OVERSHOOT_PROTECTION_INITIAL_WAIT) + # Then we wait for 60 seconds more, so we at least make sure we have some change in temperature + await asyncio.sleep(60) + # Since the coordinator doesn't support modulation management, so we need to fall back to find it with modulation if not self._coordinator.supports_relative_modulation_management or self._coordinator.relative_modulation_value > 0: return await self._calculate_with_no_modulation_management() @@ -44,7 +47,6 @@ async def calculate(self) -> float | None: async def _calculate_with_zero_modulation(self) -> float: _LOGGER.info("Running calculation with zero modulation") - await self._coordinator.async_set_control_max_relative_modulation(MINIMUM_RELATIVE_MOD) try: return await asyncio.wait_for( From c223e0dc330d2f7bc2179b564572ecd984327c28 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 10 Apr 2024 18:39:44 +0200 Subject: [PATCH 037/213] Wait a bit longer before continuing when waiting for a stable temperature --- custom_components/sat/overshoot_protection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 60ee1ff3..c74d3ce7 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -97,5 +97,5 @@ async def _wait_for_stable_temperature(self, max_modulation: float) -> float: else: await self._coordinator.async_set_control_setpoint(OVERSHOOT_PROTECTION_SETPOINT) - await asyncio.sleep(2) + await asyncio.sleep(5) await self._coordinator.async_control_heating_loop() From 27adf705ddfabd782a084848e2b5a197f9692881 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 10 Apr 2024 20:42:16 +0200 Subject: [PATCH 038/213] Add some extra debug statements --- custom_components/sat/overshoot_protection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index c74d3ce7..7c038094 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -85,8 +85,9 @@ async def _wait_for_stable_temperature(self, max_modulation: float) -> float: while True: actual_temperature = float(self._coordinator.boiler_temperature) average_temperature = self._alpha * actual_temperature + (1 - self._alpha) * previous_average_temperature + error_value = abs(actual_temperature - previous_average_temperature) - if previous_average_temperature is not None and abs(actual_temperature - previous_average_temperature) <= DEADBAND: + if previous_average_temperature is not None and error_value <= DEADBAND: _LOGGER.info("Stable temperature reached: %s", actual_temperature) return actual_temperature @@ -99,3 +100,4 @@ async def _wait_for_stable_temperature(self, max_modulation: float) -> float: await asyncio.sleep(5) await self._coordinator.async_control_heating_loop() + _LOGGER.info("Current temperature: %s, error: %s", actual_temperature, error_value) From 26cac7466591bc1bd95ef561b8f3298af6be9dfb Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Mon, 13 May 2024 23:55:30 +0200 Subject: [PATCH 039/213] Do not change the HVAC mode when it's already off --- custom_components/sat/climate.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 493126f9..1cf89cb2 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -960,10 +960,6 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: self._attr_preset_mode = PRESET_NONE await self.async_set_target_temperature(self._pre_custom_temperature) else: - # Set the HVAC mode to `HEAT` if it is currently `OFF` - if self.hvac_mode == HVACMode.OFF: - await self.async_set_hvac_mode(HVACMode.HEAT) - # Save the current target temperature if the preset mode is being set for the first time if self._attr_preset_mode == PRESET_NONE: self._pre_custom_temperature = self._target_temperature From c33fa79ef05fa72025d9d002a74c1ddcd43f5320 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Thu, 1 Aug 2024 17:30:06 +0200 Subject: [PATCH 040/213] Add support for heating mode --- custom_components/sat/climate.py | 4 ++++ custom_components/sat/config_flow.py | 8 ++++++++ custom_components/sat/const.py | 5 +++++ custom_components/sat/translations/de.json | 3 ++- custom_components/sat/translations/en.json | 3 ++- custom_components/sat/translations/es.json | 3 ++- custom_components/sat/translations/fr.json | 3 ++- custom_components/sat/translations/it.json | 3 ++- custom_components/sat/translations/nl.json | 3 ++- 9 files changed, 29 insertions(+), 6 deletions(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 1cf89cb2..214ba9a0 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -162,6 +162,7 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn self._overshoot_protection = bool(config_entry.data.get(CONF_OVERSHOOT_PROTECTION)) # User Configuration + self._heating_mode = str(config_entry.options.get(CONF_HEATING_MODE)) self._thermal_comfort = bool(config_options.get(CONF_THERMAL_COMFORT)) self._climate_valve_offset = float(config_options.get(CONF_CLIMATE_VALVE_OFFSET)) self._target_temperature_step = float(config_options.get(CONF_TARGET_TEMPERATURE_STEP)) @@ -460,6 +461,9 @@ def hvac_action(self): @property def max_error(self) -> float: + if self._heating_mode == HEATING_MODE_ECO: + return self.error + return max([self.error] + self.climate_errors) @property diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index b144c842..f990e0b4 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -459,6 +459,14 @@ async def async_step_general(self, _user_input: dict[str, Any] | None = None) -> ]) ) + if len(self._config_entry.data.get(CONF_SECONDARY_CLIMATES, [])) > 0: + schema[vol.Required(CONF_HEATING_MODE, default=str(options[CONF_HEATING_MODE]))] = selector.SelectSelector( + selector.SelectSelectorConfig(mode=SelectSelectorMode.DROPDOWN, options=[ + selector.SelectOptionDict(value=HEATING_MODE_COMFORT, label="Comfort"), + selector.SelectOptionDict(value=HEATING_MODE_ECO, label="Eco"), + ]) + ) + schema[vol.Required(CONF_MAXIMUM_SETPOINT, default=maximum_setpoint)] = selector.NumberSelector( selector.NumberSelectorConfig(min=10, max=100, step=1, unit_of_measurement="°C") ) diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index f5a69675..05485cee 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -62,6 +62,7 @@ CONF_DYNAMIC_MINIMUM_SETPOINT = "dynamic_minimum_setpoint" CONF_MINIMUM_SETPOINT_ADJUSTMENT_FACTOR = "minimum_setpoint_adjustment_factor" +CONF_HEATING_MODE = "heating_mode" CONF_HEATING_SYSTEM = "heating_system" CONF_HEATING_CURVE_VERSION = "heating_curve_version" CONF_HEATING_CURVE_COEFFICIENT = "heating_curve_coefficient" @@ -82,6 +83,9 @@ HEATING_SYSTEM_RADIATORS = "radiators" HEATING_SYSTEM_UNDERFLOOR = "underfloor" +HEATING_MODE_ECO = "eco" +HEATING_MODE_COMFORT = "comfort" + OPTIONS_DEFAULTS = { CONF_MODE: MODE_SERIAL, CONF_PROPORTIONAL: "45", @@ -132,6 +136,7 @@ CONF_HEATING_CURVE_VERSION: 3, CONF_HEATING_CURVE_COEFFICIENT: 1.0, + CONF_HEATING_MODE: HEATING_MODE_COMFORT, CONF_HEATING_SYSTEM: HEATING_SYSTEM_RADIATORS, CONF_PID_CONTROLLER_VERSION: 2, diff --git a/custom_components/sat/translations/de.json b/custom_components/sat/translations/de.json index db4050f9..7f06715c 100644 --- a/custom_components/sat/translations/de.json +++ b/custom_components/sat/translations/de.json @@ -168,7 +168,8 @@ "pid_controller_version": "PID-Reglerversion", "proportional": "Proportional (kP)", "sync_with_thermostat": "Sollwert mit Thermostat synchronisieren", - "window_sensors": "Kontaktsensoren" + "window_sensors": "Kontaktsensoren", + "heating_mode": "Heizbetrieb" }, "data_description": { "automatic_gains_value": "Der Wert, der für automatische Verstärkungen im PID-Regler verwendet wird.", diff --git a/custom_components/sat/translations/en.json b/custom_components/sat/translations/en.json index aad9fafe..0fbe7a1b 100644 --- a/custom_components/sat/translations/en.json +++ b/custom_components/sat/translations/en.json @@ -168,7 +168,8 @@ "pid_controller_version": "PID Controller Version", "proportional": "Proportional (kP)", "sync_with_thermostat": "Synchronize setpoint with thermostat", - "window_sensors": "Contact Sensors" + "window_sensors": "Contact Sensors", + "heating_mode": "Heating Mode" }, "data_description": { "automatic_gains_value": "The value used for automatic gains in the PID controller.", diff --git a/custom_components/sat/translations/es.json b/custom_components/sat/translations/es.json index 0ed9ed17..dc3d84b5 100644 --- a/custom_components/sat/translations/es.json +++ b/custom_components/sat/translations/es.json @@ -168,7 +168,8 @@ "pid_controller_version": "Versión del controlador PID", "proportional": "Proporcional (kP)", "sync_with_thermostat": "Sincronizar punto de ajuste con el termostato", - "window_sensors": "Sensores de Contacto" + "window_sensors": "Sensores de Contacto", + "heating_mode": "Modo de calefacción" }, "data_description": { "automatic_gains_value": "El valor utilizado para las ganancias automáticas en el controlador PID.", diff --git a/custom_components/sat/translations/fr.json b/custom_components/sat/translations/fr.json index d787ab8c..a053c16e 100644 --- a/custom_components/sat/translations/fr.json +++ b/custom_components/sat/translations/fr.json @@ -168,7 +168,8 @@ "pid_controller_version": "Version du contrôleur PID", "proportional": "Proportionnel (kP)", "sync_with_thermostat": "Synchroniser le point de consigne avec le thermostat", - "window_sensors": "Capteurs de Contact" + "window_sensors": "Capteurs de Contact", + "heating_mode": "Mode chauffage" }, "data_description": { "automatic_gains_value": "La valeur utilisée pour les gains automatiques dans le régulateur PID.", diff --git a/custom_components/sat/translations/it.json b/custom_components/sat/translations/it.json index 0188972e..80d12f06 100644 --- a/custom_components/sat/translations/it.json +++ b/custom_components/sat/translations/it.json @@ -168,7 +168,8 @@ "pid_controller_version": "Versione del controllore PID", "proportional": "Proporzionale (kP)", "sync_with_thermostat": "Sincronizza setpoint con termostato", - "window_sensors": "Sensori Contatto" + "window_sensors": "Sensori Contatto", + "heating_mode": "Modalità riscaldamento" }, "data_description": { "automatic_gains_value": "Il valore utilizzato per i guadagni automatici nel controllore PID.", diff --git a/custom_components/sat/translations/nl.json b/custom_components/sat/translations/nl.json index eb8b1bf9..a21663b2 100644 --- a/custom_components/sat/translations/nl.json +++ b/custom_components/sat/translations/nl.json @@ -168,7 +168,8 @@ "pid_controller_version": "Versie van de PID-regelaar", "proportional": "Proportioneel (kP)", "sync_with_thermostat": "Synchroniseer setpoint met thermostaat", - "window_sensors": "Contact Sensoren" + "window_sensors": "Contact Sensoren", + "heating_mode": "Verwarmingsmodus" }, "data_description": { "automatic_gains_value": "De waarde die wordt gebruikt voor automatische versterkingen in de PID-regelaar.", From caed09e884ffd474179facc2eca9178c6077c78d Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Thu, 1 Aug 2024 17:35:09 +0200 Subject: [PATCH 041/213] Add missing aiodhcpwatcher --- requirements_test.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_test.txt b/requirements_test.txt index 6871af61..fc866e30 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,6 +5,7 @@ pytest-homeassistant-custom-component homeassistant aiohttp_cors aiodiscover +aiodhcpwatcher freezegun pyotgw scapy From fdf48d74d82475cb897ac50ad5eec9f31a4b20d5 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Thu, 22 Aug 2024 17:45:14 +0200 Subject: [PATCH 042/213] Cleanup and make only use of "async_forward_entry_setups" --- custom_components/sat/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index 6d73c92e..3472e0d6 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -14,6 +14,7 @@ from .coordinator import SatDataUpdateCoordinatorFactory _LOGGER: logging.Logger = logging.getLogger(__name__) +PLATFORMS = [CLIMATE_DOMAIN, SENSOR_DOMAIN, NUMBER_DOMAIN, BINARY_SENSOR_DOMAIN] async def async_setup_entry(_hass: HomeAssistant, _entry: ConfigEntry): @@ -33,9 +34,8 @@ async def async_setup_entry(_hass: HomeAssistant, _entry: ConfigEntry): hass=_hass, data=_entry.data, options=_entry.options, mode=_entry.data.get(CONF_MODE), device=_entry.data.get(CONF_DEVICE) ) - # Forward entry setup for climate and other platforms - await _entry.async_create_task(_hass, _hass.config_entries.async_forward_entry_setup(_entry, CLIMATE_DOMAIN)) - await _entry.async_create_task(_hass, _hass.config_entries.async_forward_entry_setups(_entry, [SENSOR_DOMAIN, NUMBER_DOMAIN, BINARY_SENSOR_DOMAIN])) + # Forward entry setup for used platforms + await _entry.async_create_task(_hass, _hass.config_entries.async_forward_entry_setups(_entry, PLATFORMS)) # Add an update listener for this entry _entry.async_on_unload(_entry.add_update_listener(async_reload_entry)) @@ -54,9 +54,8 @@ async def async_unload_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool: await _hass.data[DOMAIN][_entry.entry_id][COORDINATOR].async_will_remove_from_hass(climate) unloaded = all( - await asyncio.gather( - _hass.config_entries.async_unload_platforms(_entry, [CLIMATE_DOMAIN, SENSOR_DOMAIN, NUMBER_DOMAIN, BINARY_SENSOR_DOMAIN]), - ) + # Forward entry unload for used platforms + await asyncio.gather(_hass.config_entries.async_unload_platforms(_entry, PLATFORMS)) ) # Remove the entry from the data dictionary if all components are unloaded successfully From 7f6317ce0490b60663fe1ac078f24df2caf41445 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 27 Aug 2024 18:44:49 +0200 Subject: [PATCH 043/213] Add missing abstract method property for serial connection --- custom_components/sat/serial/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/custom_components/sat/serial/__init__.py b/custom_components/sat/serial/__init__.py index 2f093ab9..cf7e4813 100644 --- a/custom_components/sat/serial/__init__.py +++ b/custom_components/sat/serial/__init__.py @@ -11,6 +11,7 @@ from serial import SerialException from ..coordinator import DeviceState, SatDataUpdateCoordinator +from ..mqtt import DATA_SLAVE_MEMBER_ID if TYPE_CHECKING: from ..climate import SatClimate @@ -135,6 +136,13 @@ def maximum_relative_modulation_value(self) -> float | None: return super().maximum_relative_modulation_value + @property + def member_id(self) -> int | None: + if (value := self.get(DATA_SLAVE_MEMBER_ID)) is not None: + return int(value) + + return None + @property def flame_active(self) -> bool: return bool(self.get(DATA_SLAVE_FLAME_ON)) From 958f9c6b33454e480a4540d8b1f45e6744d83d03 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 27 Aug 2024 18:48:04 +0200 Subject: [PATCH 044/213] Typo? --- custom_components/sat/serial/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/sat/serial/__init__.py b/custom_components/sat/serial/__init__.py index cf7e4813..c4f608a4 100644 --- a/custom_components/sat/serial/__init__.py +++ b/custom_components/sat/serial/__init__.py @@ -11,7 +11,6 @@ from serial import SerialException from ..coordinator import DeviceState, SatDataUpdateCoordinator -from ..mqtt import DATA_SLAVE_MEMBER_ID if TYPE_CHECKING: from ..climate import SatClimate @@ -138,7 +137,7 @@ def maximum_relative_modulation_value(self) -> float | None: @property def member_id(self) -> int | None: - if (value := self.get(DATA_SLAVE_MEMBER_ID)) is not None: + if (value := self.get(DATA_SLAVE_MEMBERID)) is not None: return int(value) return None From f28a932c5f32c4cf6bf5eb8c977e246788ee9180 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 27 Aug 2024 18:49:35 +0200 Subject: [PATCH 045/213] Make the mqtt variable name consistent --- custom_components/sat/mqtt/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 6afd4a09..d2902465 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -24,7 +24,7 @@ DATA_RETURN_TEMPERATURE = "Tret" DATA_DHW_ENABLE = "domestichotwater" DATA_CENTRAL_HEATING = "centralheating" -DATA_SLAVE_MEMBER_ID = "slave_memberid_code" +DATA_SLAVE_MEMBERID = "slave_memberid_code" DATA_BOILER_CAPACITY = "MaxCapacityMinModLevel_hb_u8" DATA_REL_MIN_MOD_LEVEL = "MaxCapacityMinModLevel_lb_u8" DATA_REL_MIN_MOD_LEVELL = "MaxCapacityMinModLevell_lb_u8" @@ -156,7 +156,7 @@ def maximum_relative_modulation_value(self) -> float | None: @property def member_id(self) -> int | None: - if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_SLAVE_MEMBER_ID)) is not None: + if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_SLAVE_MEMBERID)) is not None: return int(value) return None From 09a03f555770d8c89a684df128774842d21214a8 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 27 Aug 2024 18:51:13 +0200 Subject: [PATCH 046/213] Align MQTT coordinator method naming with SERIAL coordinator --- custom_components/sat/mqtt/__init__.py | 40 +++++++++++++++----------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index d2902465..cc3d43b7 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations, annotations import logging -from typing import TYPE_CHECKING, Mapping, Any +from typing import TYPE_CHECKING, Mapping, Any, Optional from homeassistant.components import mqtt from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -70,93 +70,93 @@ def supports_relative_modulation_management(self): @property def device_active(self) -> bool: - return self._get_entity_state(BINARY_SENSOR_DOMAIN, DATA_CENTRAL_HEATING) == DeviceState.ON + return self.get(BINARY_SENSOR_DOMAIN, DATA_CENTRAL_HEATING) == DeviceState.ON @property def flame_active(self) -> bool: - return self._get_entity_state(BINARY_SENSOR_DOMAIN, DATA_FLAME_ACTIVE) == DeviceState.ON + return self.get(BINARY_SENSOR_DOMAIN, DATA_FLAME_ACTIVE) == DeviceState.ON @property def hot_water_active(self) -> bool: - return self._get_entity_state(BINARY_SENSOR_DOMAIN, DATA_DHW_ENABLE) == DeviceState.ON + return self.get(BINARY_SENSOR_DOMAIN, DATA_DHW_ENABLE) == DeviceState.ON @property def setpoint(self) -> float | None: - if (setpoint := self._get_entity_state(SENSOR_DOMAIN, DATA_CONTROL_SETPOINT)) is not None: + if (setpoint := self.get(SENSOR_DOMAIN, DATA_CONTROL_SETPOINT)) is not None: return float(setpoint) return None @property def hot_water_setpoint(self) -> float | None: - if (setpoint := self._get_entity_state(SENSOR_DOMAIN, DATA_DHW_SETPOINT)) is not None: + if (setpoint := self.get(SENSOR_DOMAIN, DATA_DHW_SETPOINT)) is not None: return float(setpoint) return super().hot_water_setpoint @property def minimum_hot_water_setpoint(self) -> float: - if (setpoint := self._get_entity_state(SENSOR_DOMAIN, DATA_DHW_SETPOINT_MINIMUM)) is not None: + if (setpoint := self.get(SENSOR_DOMAIN, DATA_DHW_SETPOINT_MINIMUM)) is not None: return float(setpoint) return super().minimum_hot_water_setpoint @property def maximum_hot_water_setpoint(self) -> float | None: - if (setpoint := self._get_entity_state(SENSOR_DOMAIN, DATA_DHW_SETPOINT_MAXIMUM)) is not None: + if (setpoint := self.get(SENSOR_DOMAIN, DATA_DHW_SETPOINT_MAXIMUM)) is not None: return float(setpoint) return super().maximum_hot_water_setpoint @property def boiler_temperature(self) -> float | None: - if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_BOILER_TEMPERATURE)) is not None: + if (value := self.get(SENSOR_DOMAIN, DATA_BOILER_TEMPERATURE)) is not None: return float(value) return super().boiler_temperature @property def return_temperature(self) -> float | None: - if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_RETURN_TEMPERATURE)) is not None: + if (value := self.get(SENSOR_DOMAIN, DATA_RETURN_TEMPERATURE)) is not None: return float(value) return super().return_temperature @property def relative_modulation_value(self) -> float | None: - if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_REL_MOD_LEVEL)) is not None: + if (value := self.get(SENSOR_DOMAIN, DATA_REL_MOD_LEVEL)) is not None: return float(value) return super().relative_modulation_value @property def boiler_capacity(self) -> float | None: - if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_BOILER_CAPACITY)) is not None: + if (value := self.get(SENSOR_DOMAIN, DATA_BOILER_CAPACITY)) is not None: return float(value) return super().boiler_capacity @property def minimum_relative_modulation_value(self) -> float | None: - if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_REL_MIN_MOD_LEVEL)) is not None: + if (value := self.get(SENSOR_DOMAIN, DATA_REL_MIN_MOD_LEVEL)) is not None: return float(value) # Legacy - if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_REL_MIN_MOD_LEVELL)) is not None: + if (value := self.get(SENSOR_DOMAIN, DATA_REL_MIN_MOD_LEVELL)) is not None: return float(value) return super().minimum_relative_modulation_value @property def maximum_relative_modulation_value(self) -> float | None: - if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_MAX_REL_MOD_LEVEL_SETTING)) is not None: + if (value := self.get(SENSOR_DOMAIN, DATA_MAX_REL_MOD_LEVEL_SETTING)) is not None: return float(value) return super().maximum_relative_modulation_value @property def member_id(self) -> int | None: - if (value := self._get_entity_state(SENSOR_DOMAIN, DATA_SLAVE_MEMBERID)) is not None: + if (value := self.get(SENSOR_DOMAIN, DATA_SLAVE_MEMBERID)) is not None: return int(value) return None @@ -232,7 +232,13 @@ async def async_set_control_max_setpoint(self, value: float) -> None: await super().async_set_control_max_setpoint(value) - def _get_entity_state(self, domain: str, key: str): + def get(self, domain: str, key: str) -> Optional[Any]: + """Get the value for the given `key` from the boiler data. + + :param domain: Domain of where this value is located. + :param key: Key of the value to retrieve from the boiler data. + :return: Value for the given key from the boiler data, or None if the boiler data or the value are not available. + """ entity_id = self._get_entity_id(domain, key) if entity_id is None: return None From ff8faf323af077d5b024d43f4fff46df83747d3d Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 30 Aug 2024 21:52:01 +0200 Subject: [PATCH 047/213] Added experimental new overshoot protection flow --- custom_components/sat/climate.py | 4 - custom_components/sat/config_flow.py | 2 +- custom_components/sat/const.py | 11 ++- custom_components/sat/overshoot_protection.py | 88 +++++++++---------- 4 files changed, 52 insertions(+), 53 deletions(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 214ba9a0..e2172f4e 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -586,10 +586,6 @@ def _calculate_control_setpoint(self) -> float: # Combine the heating curve value and the calculated output from the pid controller requested_setpoint = self.requested_setpoint - # Make sure we are above the base setpoint when we are below the target temperature - if self.max_error > 0: - requested_setpoint = max(requested_setpoint, self.heating_curve.value) - # Ensure setpoint is limited to our max return min(requested_setpoint, self._coordinator.maximum_setpoint) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index f990e0b4..de99ed38 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -370,7 +370,7 @@ async def async_step_overshoot_protection(self, _user_input: dict[str, Any] | No step_id="overshoot_protection", data_schema=vol.Schema({ vol.Required(CONF_MINIMUM_SETPOINT, default=self.data.get(CONF_MINIMUM_SETPOINT, OPTIONS_DEFAULTS[CONF_MINIMUM_SETPOINT])): selector.NumberSelector( - selector.NumberSelectorConfig(min=MINIMUM_SETPOINT, max=OVERSHOOT_PROTECTION_SETPOINT, step=1, unit_of_measurement="°C") + selector.NumberSelectorConfig(min=MINIMUM_SETPOINT, max=MAXIMUM_SETPOINT, step=1, unit_of_measurement="°C") ), }) ) diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index 05485cee..29b05d97 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -16,12 +16,11 @@ HEATER_STARTUP_TIMEFRAME = 180 MINIMUM_SETPOINT = 10 +MAXIMUM_SETPOINT = 65 MINIMUM_RELATIVE_MOD = 0 MAXIMUM_RELATIVE_MOD = 100 MAX_BOILER_TEMPERATURE_AGE = 60 -OVERSHOOT_PROTECTION_SETPOINT = 75 -OVERSHOOT_PROTECTION_REQUIRED_DATASET = 40 # Configuration and options CONF_MODE = "mode" @@ -142,6 +141,14 @@ CONF_PID_CONTROLLER_VERSION: 2, } +# Overshoot protection +OVERSHOOT_PROTECTION_REQUIRED_DATASET = 40 +OVERSHOOT_PROTECTION_SETPOINT = { + HEATING_SYSTEM_HEAT_PUMP: 40, + HEATING_SYSTEM_RADIATORS: 62, + HEATING_SYSTEM_UNDERFLOOR: 45, +} + # Storage STORAGE_OVERSHOOT_PROTECTION_VALUE = "overshoot_protection_value" diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 7c038094..6d9afeac 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -11,75 +11,54 @@ class OvershootProtection: - def __init__(self, coordinator: SatDataUpdateCoordinator): + def __init__(self, coordinator: SatDataUpdateCoordinator, heating_system: str): self._alpha = 0.2 self._coordinator = coordinator + self._setpoint = OVERSHOOT_PROTECTION_SETPOINT[heating_system] async def calculate(self) -> float | None: - _LOGGER.info("Starting calculation") + try: + _LOGGER.info("Starting calculation") - await self._coordinator.async_set_heater_state(DeviceState.ON) - await self._coordinator.async_set_control_setpoint(OVERSHOOT_PROTECTION_SETPOINT) - await self._coordinator.async_set_control_max_relative_modulation(MINIMUM_RELATIVE_MOD) + # Turn on the heater + await self._coordinator.async_set_heater_state(DeviceState.ON) - try: # First wait for a flame await asyncio.wait_for(self._wait_for_flame(), timeout=OVERSHOOT_PROTECTION_INITIAL_WAIT) - # Then we wait for 60 seconds more, so we at least make sure we have some change in temperature - await asyncio.sleep(60) + # Then we wait until we reach the target setpoint temperature + await asyncio.wait_for(self._wait_for_stable_temperature(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) + + # Then we wait for 5 minutes more, so we at least make sure we are stable + await asyncio.sleep(300) - # Since the coordinator doesn't support modulation management, so we need to fall back to find it with modulation - if not self._coordinator.supports_relative_modulation_management or self._coordinator.relative_modulation_value > 0: - return await self._calculate_with_no_modulation_management() + # Then we wait for a stable relative modulation value + relative_modulation_value = await asyncio.wait_for(self._wait_for_relative_modulation(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) - # Run with maximum power of the boiler, zero modulation. - return await self._calculate_with_zero_modulation() + # Calculate the new overshoot protection value + return (100 - relative_modulation_value / 100) * self._setpoint except asyncio.TimeoutError: - _LOGGER.warning("Timed out waiting for stable temperature") + _LOGGER.warning("Timed out waiting for calculation") return None except asyncio.CancelledError as exception: await self._coordinator.async_set_heater_state(DeviceState.OFF) await self._coordinator.async_set_control_setpoint(MINIMUM_SETPOINT) - await self._coordinator.async_set_control_max_relative_modulation(MAXIMUM_RELATIVE_MOD) raise exception - async def _calculate_with_zero_modulation(self) -> float: - _LOGGER.info("Running calculation with zero modulation") - - try: - return await asyncio.wait_for( - self._wait_for_stable_temperature(0), - timeout=OVERSHOOT_PROTECTION_TIMEOUT, - ) - except asyncio.TimeoutError: - _LOGGER.warning("Timed out waiting for stable temperature") - - async def _calculate_with_no_modulation_management(self) -> float: - _LOGGER.info("Running calculation with no modulation management") - - try: - return await asyncio.wait_for( - self._wait_for_stable_temperature(100), - timeout=OVERSHOOT_PROTECTION_TIMEOUT, - ) - except asyncio.TimeoutError: - _LOGGER.warning("Timed out waiting for stable temperature") - - async def _wait_for_flame(self): + async def _wait_for_flame(self) -> None: while True: if bool(self._coordinator.flame_active): _LOGGER.info("Heating system has started to run") break _LOGGER.warning("Heating system is not running yet") - await self._coordinator.async_set_control_setpoint(OVERSHOOT_PROTECTION_SETPOINT) + await self._coordinator.async_set_control_setpoint(self._setpoint) await asyncio.sleep(5) await self._coordinator.async_control_heating_loop() - async def _wait_for_stable_temperature(self, max_modulation: float) -> float: + async def _wait_for_stable_temperature(self) -> None: previous_average_temperature = float(self._coordinator.boiler_temperature) while True: @@ -89,15 +68,32 @@ async def _wait_for_stable_temperature(self, max_modulation: float) -> float: if previous_average_temperature is not None and error_value <= DEADBAND: _LOGGER.info("Stable temperature reached: %s", actual_temperature) - return actual_temperature + break previous_average_temperature = average_temperature - - if max_modulation > 0: - await self._coordinator.async_set_control_setpoint(actual_temperature) - else: - await self._coordinator.async_set_control_setpoint(OVERSHOOT_PROTECTION_SETPOINT) + await self._coordinator.async_set_control_setpoint(self._setpoint) + await self._coordinator.async_set_control_max_relative_modulation(MAXIMUM_RELATIVE_MOD) await asyncio.sleep(5) await self._coordinator.async_control_heating_loop() _LOGGER.info("Current temperature: %s, error: %s", actual_temperature, error_value) + + async def _wait_for_relative_modulation(self) -> float: + previous_average_value = float(self._coordinator.relative_modulation_value) + + while True: + actual_value = float(self._coordinator.relative_modulation_value) + average_value = self._alpha * actual_value + (1 - self._alpha) * previous_average_value + error_value = abs(actual_value - previous_average_value) + + if previous_average_value is not None and error_value <= DEADBAND: + _LOGGER.info("Relative Modulation reached: %s", actual_value) + return actual_value + + previous_average_value = average_value + await self._coordinator.async_set_control_setpoint(self._setpoint) + await self._coordinator.async_set_control_max_relative_modulation(MAXIMUM_RELATIVE_MOD) + + await asyncio.sleep(5) + await self._coordinator.async_control_heating_loop() + _LOGGER.info("Relative Modulation: %s, error: %s", actual_value, error_value) From 1c59b43e2fe477b405776c60b105fae79b861d38 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 30 Aug 2024 21:53:07 +0200 Subject: [PATCH 048/213] Name fixing --- custom_components/sat/overshoot_protection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 6d9afeac..1b06dabf 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -33,7 +33,7 @@ async def calculate(self) -> float | None: await asyncio.sleep(300) # Then we wait for a stable relative modulation value - relative_modulation_value = await asyncio.wait_for(self._wait_for_relative_modulation(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) + relative_modulation_value = await asyncio.wait_for(self._wait_for_stable_relative_modulation(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) # Calculate the new overshoot protection value return (100 - relative_modulation_value / 100) * self._setpoint @@ -78,7 +78,7 @@ async def _wait_for_stable_temperature(self) -> None: await self._coordinator.async_control_heating_loop() _LOGGER.info("Current temperature: %s, error: %s", actual_temperature, error_value) - async def _wait_for_relative_modulation(self) -> float: + async def _wait_for_stable_relative_modulation(self) -> float: previous_average_value = float(self._coordinator.relative_modulation_value) while True: From 5beaf9bc5c96a7c38c5455ac5574ffa6fc6afa33 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 30 Aug 2024 22:03:37 +0200 Subject: [PATCH 049/213] Cleanup --- custom_components/sat/overshoot_protection.py | 91 ++++++++++--------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 1b06dabf..ec200c1a 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -8,6 +8,8 @@ OVERSHOOT_PROTECTION_TIMEOUT = 7200 # Two hours in seconds OVERSHOOT_PROTECTION_INITIAL_WAIT = 180 # Three minutes in seconds +STABLE_TEMPERATURE_WAIT = 300 # Five minutes in seconds +SLEEP_INTERVAL = 5 # Sleep interval in seconds class OvershootProtection: @@ -18,82 +20,81 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, heating_system: str): async def calculate(self) -> float | None: try: - _LOGGER.info("Starting calculation") + _LOGGER.info("Starting overshoot protection calculation") - # Turn on the heater await self._coordinator.async_set_heater_state(DeviceState.ON) - # First wait for a flame + # Enforce timeouts to ensure operations do not run indefinitely await asyncio.wait_for(self._wait_for_flame(), timeout=OVERSHOOT_PROTECTION_INITIAL_WAIT) - - # Then we wait until we reach the target setpoint temperature await asyncio.wait_for(self._wait_for_stable_temperature(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) - # Then we wait for 5 minutes more, so we at least make sure we are stable - await asyncio.sleep(300) + _LOGGER.info("Waiting an additional %s seconds for stability", STABLE_TEMPERATURE_WAIT) + await asyncio.sleep(STABLE_TEMPERATURE_WAIT) - # Then we wait for a stable relative modulation value relative_modulation_value = await asyncio.wait_for(self._wait_for_stable_relative_modulation(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) - # Calculate the new overshoot protection value - return (100 - relative_modulation_value / 100) * self._setpoint + return self._calculate_overshoot_value(relative_modulation_value) except asyncio.TimeoutError: - _LOGGER.warning("Timed out waiting for calculation") + _LOGGER.warning("Timed out during overshoot protection calculation") return None except asyncio.CancelledError as exception: - await self._coordinator.async_set_heater_state(DeviceState.OFF) - await self._coordinator.async_set_control_setpoint(MINIMUM_SETPOINT) - + await self._shutdown_on_cancel() raise exception async def _wait_for_flame(self) -> None: - while True: - if bool(self._coordinator.flame_active): - _LOGGER.info("Heating system has started to run") - break + while not bool(self._coordinator.flame_active): + _LOGGER.warning("Waiting for heating system to start") + await self._trigger_heating_cycle() - _LOGGER.warning("Heating system is not running yet") - await self._coordinator.async_set_control_setpoint(self._setpoint) - - await asyncio.sleep(5) - await self._coordinator.async_control_heating_loop() + _LOGGER.info("Heating system has started") async def _wait_for_stable_temperature(self) -> None: previous_average_temperature = float(self._coordinator.boiler_temperature) while True: - actual_temperature = float(self._coordinator.boiler_temperature) - average_temperature = self._alpha * actual_temperature + (1 - self._alpha) * previous_average_temperature - error_value = abs(actual_temperature - previous_average_temperature) + current_temperature = float(self._coordinator.boiler_temperature) + average_temperature, error_value = self._calculate_exponential_moving_average(previous_average_temperature, current_temperature) if previous_average_temperature is not None and error_value <= DEADBAND: - _LOGGER.info("Stable temperature reached: %s", actual_temperature) - break + _LOGGER.info("Stable temperature reached: %s°C", current_temperature) + return previous_average_temperature = average_temperature - await self._coordinator.async_set_control_setpoint(self._setpoint) - await self._coordinator.async_set_control_max_relative_modulation(MAXIMUM_RELATIVE_MOD) - - await asyncio.sleep(5) - await self._coordinator.async_control_heating_loop() - _LOGGER.info("Current temperature: %s, error: %s", actual_temperature, error_value) + await self._trigger_heating_cycle() + _LOGGER.debug("Temperature: %s°C, Error: %s°C", current_temperature, error_value) async def _wait_for_stable_relative_modulation(self) -> float: previous_average_value = float(self._coordinator.relative_modulation_value) while True: - actual_value = float(self._coordinator.relative_modulation_value) - average_value = self._alpha * actual_value + (1 - self._alpha) * previous_average_value - error_value = abs(actual_value - previous_average_value) + current_value = float(self._coordinator.relative_modulation_value) + average_value, error_value = self._calculate_exponential_moving_average(previous_average_value, current_value) if previous_average_value is not None and error_value <= DEADBAND: - _LOGGER.info("Relative Modulation reached: %s", actual_value) - return actual_value + _LOGGER.info("Stable relative modulation reached: %s%%", current_value) + return current_value previous_average_value = average_value - await self._coordinator.async_set_control_setpoint(self._setpoint) - await self._coordinator.async_set_control_max_relative_modulation(MAXIMUM_RELATIVE_MOD) - - await asyncio.sleep(5) - await self._coordinator.async_control_heating_loop() - _LOGGER.info("Relative Modulation: %s, error: %s", actual_value, error_value) + await self._trigger_heating_cycle() + _LOGGER.debug("Relative Modulation: %s%%, Error: %s%%", current_value, error_value) + + def _calculate_overshoot_value(self, relative_modulation_value: float) -> float: + overshoot_value = (100 - relative_modulation_value) / 100 * self._setpoint + _LOGGER.info("Calculated overshoot value: %s", overshoot_value) + return overshoot_value + + def _calculate_exponential_moving_average(self, previous_average: float, current_value: float) -> tuple[float, float]: + average_value = self._alpha * current_value + (1 - self._alpha) * previous_average + error_value = abs(current_value - previous_average) + return average_value, error_value + + async def _trigger_heating_cycle(self) -> None: + await self._coordinator.async_set_control_setpoint(self._setpoint) + await self._coordinator.async_set_control_max_relative_modulation(MAXIMUM_RELATIVE_MOD) + await asyncio.sleep(SLEEP_INTERVAL) + await self._coordinator.async_control_heating_loop() + + async def _shutdown_on_cancel(self) -> None: + _LOGGER.info("Calculation cancelled, shutting down heating system") + await self._coordinator.async_set_heater_state(DeviceState.OFF) + await self._coordinator.async_set_control_setpoint(MINIMUM_SETPOINT) From 27dc0215bcc73fd8ce69cc9de5105efeae2c89db Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 30 Aug 2024 22:06:51 +0200 Subject: [PATCH 050/213] Drop verbose method --- custom_components/sat/overshoot_protection.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index ec200c1a..f77b9f3d 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -38,7 +38,10 @@ async def calculate(self) -> float | None: _LOGGER.warning("Timed out during overshoot protection calculation") return None except asyncio.CancelledError as exception: - await self._shutdown_on_cancel() + _LOGGER.info("Calculation cancelled, shutting down heating system") + await self._coordinator.async_set_heater_state(DeviceState.OFF) + await self._coordinator.async_set_control_setpoint(MINIMUM_SETPOINT) + raise exception async def _wait_for_flame(self) -> None: @@ -93,8 +96,3 @@ async def _trigger_heating_cycle(self) -> None: await self._coordinator.async_set_control_max_relative_modulation(MAXIMUM_RELATIVE_MOD) await asyncio.sleep(SLEEP_INTERVAL) await self._coordinator.async_control_heating_loop() - - async def _shutdown_on_cancel(self) -> None: - _LOGGER.info("Calculation cancelled, shutting down heating system") - await self._coordinator.async_set_heater_state(DeviceState.OFF) - await self._coordinator.async_set_control_setpoint(MINIMUM_SETPOINT) From c67f29144271c48a02fe0bb70bd885399474ead4 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 4 Sep 2024 20:57:46 +0200 Subject: [PATCH 051/213] Pass the task to the progress bar (new parameter?) --- custom_components/sat/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index de99ed38..f349945e 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -336,6 +336,7 @@ async def start_calibration(): return self.async_show_progress( step_id="calibrate", + task=self.calibration, progress_action="calibration", ) From 755c4c7d96f531534297bd1a8d020628b902ba5d Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 4 Sep 2024 21:00:51 +0200 Subject: [PATCH 052/213] Add missing heating_system --- custom_components/sat/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index f349945e..6dd461d5 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -314,7 +314,7 @@ async def async_step_calibrate(self, _user_input: dict[str, Any] | None = None): async def start_calibration(): try: - overshoot_protection = OvershootProtection(coordinator) + overshoot_protection = OvershootProtection(coordinator, self.data.get(CONF_HEATING_SYSTEM)) self.overshoot_protection_value = await overshoot_protection.calculate() except asyncio.TimeoutError: _LOGGER.warning("Calibration time-out.") From 94b00da03dbf7ca27e03107b9bdc77a0f61d19aa Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 4 Sep 2024 21:41:48 +0200 Subject: [PATCH 053/213] Typo --- custom_components/sat/config_flow.py | 2 +- custom_components/sat/mqtt/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 6dd461d5..71a2d3db 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -336,7 +336,7 @@ async def start_calibration(): return self.async_show_progress( step_id="calibrate", - task=self.calibration, + progress_task=self.calibration, progress_action="calibration", ) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index cc3d43b7..b64583c0 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -193,7 +193,7 @@ async def async_added_to_hass(self, climate: SatClimate) -> None: await super().async_added_to_hass(climate) - async def async_state_change_event(self, event: Event): + async def async_state_change_event(self, _event: Event): if self._listeners: self._schedule_refresh() From 6d1a4be65822381e0fc41bb50e9f61a0399bf19a Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 13:20:47 +0200 Subject: [PATCH 054/213] Add initial and very alpha support for esphome --- custom_components/sat/config_flow.py | 36 +++- custom_components/sat/const.py | 1 + custom_components/sat/coordinator.py | 30 ++- custom_components/sat/esphome/__init__.py | 222 ++++++++++++++++++++++ custom_components/sat/mqtt/__init__.py | 24 +-- 5 files changed, 283 insertions(+), 30 deletions(-) create mode 100644 custom_components/sat/esphome/__init__.py diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 71a2d3db..2a0df9bb 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -17,7 +17,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import MAJOR_VERSION, MINOR_VERSION from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector, device_registry, entity_registry from homeassistant.helpers.selector import SelectSelectorMode from homeassistant.helpers.service_info.mqtt import MqttServiceInfo @@ -59,7 +58,7 @@ def async_remove(self) -> None: if self.calibration is not None: self.calibration.cancel() - async def async_step_user(self, _user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_user(self, _user_input: dict[str, Any] | None = None): """Handle user flow.""" menu_options = [] @@ -75,7 +74,7 @@ async def async_step_user(self, _user_input: dict[str, Any] | None = None) -> Fl return self.async_show_menu(step_id="user", menu_options=menu_options) - async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo): """Handle dhcp discovery.""" _LOGGER.debug("Discovered OTGW at [socket://%s]", discovery_info.hostname) self.data[CONF_DEVICE] = f"socket://{discovery_info.hostname}:25238" @@ -129,6 +128,27 @@ async def async_step_mosquitto(self, _user_input: dict[str, Any] | None = None): }), ) + async def async_step_esphome(self, _user_input: dict[str, Any] | None = None): + self.errors = {} + + if _user_input is not None: + self.data.update(_user_input) + self.data[CONF_MODE] = MODE_ESPHOME + + return await self.async_step_sensors() + + return self.async_show_form( + step_id="esphome", + last_step=False, + errors=self.errors, + data_schema=vol.Schema({ + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_DEVICE, default=self.data.get(CONF_DEVICE)): selector.DeviceSelector( + selector.DeviceSelectorConfig(integration="esphome") + ), + }), + ) + async def async_step_serial(self, _user_input: dict[str, Any] | None = None): self.errors = {} @@ -435,7 +455,7 @@ async def async_step_init(self, _user_input: dict[str, Any] | None = None): menu_options=menu_options ) - async def async_step_general(self, _user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_general(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: return await self.update_options(_user_input) @@ -511,7 +531,7 @@ async def async_step_general(self, _user_input: dict[str, Any] | None = None) -> return self.async_show_form(step_id="general", data_schema=vol.Schema(schema)) - async def async_step_presets(self, _user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_presets(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: return await self.update_options(_user_input) @@ -538,7 +558,7 @@ async def async_step_presets(self, _user_input: dict[str, Any] | None = None) -> }) ) - async def async_step_system_configuration(self, _user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_system_configuration(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: return await self.update_options(_user_input) @@ -553,7 +573,7 @@ async def async_step_system_configuration(self, _user_input: dict[str, Any] | No }) ) - async def async_step_advanced(self, _user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_advanced(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: return await self.update_options(_user_input) @@ -595,7 +615,7 @@ async def async_step_advanced(self, _user_input: dict[str, Any] | None = None) - data_schema=vol.Schema(schema) ) - async def update_options(self, _user_input) -> FlowResult: + async def update_options(self, _user_input): self._options.update(_user_input) return self.async_create_entry(title=self._config_entry.data[CONF_NAME], data=self._options) diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index 29b05d97..62dbe44e 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -10,6 +10,7 @@ MODE_MQTT = "mqtt" MODE_SWITCH = "switch" MODE_SERIAL = "serial" +MODE_ESPHOME = "esphome" MODE_SIMULATOR = "simulator" DEADBAND = 0.1 diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index ce72b8a1..dace81dc 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -4,9 +4,10 @@ from abc import abstractmethod from datetime import datetime, timedelta from enum import Enum -from typing import TYPE_CHECKING, Mapping, Any +from typing import TYPE_CHECKING, Mapping, Any, Optional from homeassistant.components.climate import HVACMode +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -46,6 +47,10 @@ async def resolve( from .switch import SatSwitchCoordinator return SatSwitchCoordinator(hass=hass, entity_id=device, data=data, options=options) + if mode == MODE_ESPHOME: + from .esphome import SatEspHomeCoordinator + return SatEspHomeCoordinator(hass=hass, device_id=device, data=data, options=options) + if mode == MODE_MQTT: from .mqtt import SatMqttCoordinator return await SatMqttCoordinator(hass=hass, device_id=device, data=data, options=options).boot() @@ -287,3 +292,26 @@ async def async_set_control_max_relative_modulation(self, value: int) -> None: async def async_set_control_thermostat_setpoint(self, value: float) -> None: """Control the setpoint temperature for the thermostat.""" pass + + +class SatEntityCoordinator(DataUpdateCoordinator): + def get(self, domain: str, key: str) -> Optional[Any]: + """Get the value for the given `key` from the boiler data. + + :param domain: Domain of where this value is located. + :param key: Key of the value to retrieve from the boiler data. + :return: Value for the given key from the boiler data, or None if the boiler data or the value are not available. + """ + entity_id = self._get_entity_id(domain, key) + if entity_id is None: + return None + + state = self.hass.states.get(self._get_entity_id(domain, key)) + if state is None or state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): + return None + + return state.state + + @abstractmethod + def _get_entity_id(self, domain: str, key: str): + pass diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py new file mode 100644 index 00000000..eca9d63f --- /dev/null +++ b/custom_components/sat/esphome/__init__.py @@ -0,0 +1,222 @@ +from __future__ import annotations, annotations + +import logging +from typing import TYPE_CHECKING, Mapping, Any + +from homeassistant.components import mqtt +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.esphome import DOMAIN as ESPHOME_DOMAIN +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN +from homeassistant.components.number.const import SERVICE_SET_VALUE +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON, SERVICE_TURN_OFF +from homeassistant.core import HomeAssistant, Event +from homeassistant.helpers import device_registry, entity_registry +from homeassistant.helpers.event import async_track_state_change_event + +from ..coordinator import DeviceState, SatDataUpdateCoordinator, SatEntityCoordinator + +# Sensors +DATA_FLAME_ACTIVE = "flame_active" +DATA_REL_MOD_LEVEL = "modulation" +DATA_SLAVE_MEMBERID = "boiler_member_id" +DATA_BOILER_TEMPERATURE = "boiler_temperature" +DATA_RETURN_TEMPERATURE = "return_temperature" + +DATA_DHW_SETPOINT_MINIMUM = "dhw_min_temperature" +DATA_DHW_SETPOINT_MAXIMUM = "dhw_max_temperature" + +# Switch +DATA_DHW_ENABLE = "dhw_enabled" +DATA_CENTRAL_HEATING = "ch_enabled" + +# Number +DATA_DHW_SETPOINT = "dhw_setpoint_temperature" +DATA_CONTROL_SETPOINT = "ch_setpoint_temperature" +DATA_MAX_REL_MOD_LEVEL_SETTING = "max_modulation" + +if TYPE_CHECKING: + from ..climate import SatClimate + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +class SatEspHomeCoordinator(SatDataUpdateCoordinator, SatEntityCoordinator): + """Class to manage to fetch data from the OTGW Gateway using mqtt.""" + + def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], options: Mapping[str, Any] | None = None) -> None: + super().__init__(hass, data, options) + + self.data = {} + + self._device = device_registry.async_get(hass).async_get(device_id) + self._mac_address = list(self._device.identifiers)[0][1] + + self._entity_registry = entity_registry.async_get(hass) + self._entities = entity_registry.async_entries_for_device(self._entity_registry, self._device.id) + + @property + def supports_setpoint_management(self): + return True + + @property + def supports_hot_water_setpoint_management(self): + return True + + def supports_maximum_setpoint_management(self): + return True + + @property + def supports_relative_modulation_management(self): + return True + + @property + def device_active(self) -> bool: + return self.get(BINARY_SENSOR_DOMAIN, DATA_CENTRAL_HEATING) == DeviceState.ON + + @property + def flame_active(self) -> bool: + return self.get(BINARY_SENSOR_DOMAIN, DATA_FLAME_ACTIVE) == DeviceState.ON + + @property + def hot_water_active(self) -> bool: + return self.get(BINARY_SENSOR_DOMAIN, DATA_DHW_ENABLE) == DeviceState.ON + + @property + def setpoint(self) -> float | None: + if (setpoint := self.get(SENSOR_DOMAIN, DATA_CONTROL_SETPOINT)) is not None: + return float(setpoint) + + return None + + @property + def hot_water_setpoint(self) -> float | None: + if (setpoint := self.get(SENSOR_DOMAIN, DATA_DHW_SETPOINT)) is not None: + return float(setpoint) + + return super().hot_water_setpoint + + @property + def minimum_hot_water_setpoint(self) -> float: + if (setpoint := self.get(SENSOR_DOMAIN, DATA_DHW_SETPOINT_MINIMUM)) is not None: + return float(setpoint) + + return super().minimum_hot_water_setpoint + + @property + def maximum_hot_water_setpoint(self) -> float | None: + if (setpoint := self.get(SENSOR_DOMAIN, DATA_DHW_SETPOINT_MAXIMUM)) is not None: + return float(setpoint) + + return super().maximum_hot_water_setpoint + + @property + def boiler_temperature(self) -> float | None: + if (value := self.get(SENSOR_DOMAIN, DATA_BOILER_TEMPERATURE)) is not None: + return float(value) + + return super().boiler_temperature + + @property + def return_temperature(self) -> float | None: + if (value := self.get(SENSOR_DOMAIN, DATA_RETURN_TEMPERATURE)) is not None: + return float(value) + + return super().return_temperature + + @property + def relative_modulation_value(self) -> float | None: + if (value := self.get(SENSOR_DOMAIN, DATA_REL_MOD_LEVEL)) is not None: + return float(value) + + return super().relative_modulation_value + + @property + def maximum_relative_modulation_value(self) -> float | None: + if (value := self.get(NUMBER_DOMAIN, DATA_MAX_REL_MOD_LEVEL_SETTING)) is not None: + return float(value) + + return super().maximum_relative_modulation_value + + @property + def member_id(self) -> int | None: + if (value := self.get(SENSOR_DOMAIN, DATA_SLAVE_MEMBERID)) is not None: + return int(value) + + return None + + async def async_added_to_hass(self, climate: SatClimate) -> None: + await mqtt.async_wait_for_mqtt_client(self.hass) + + # Create a list of entities that we track + entities = list(filter(lambda entity: entity is not None, [ + self._get_entity_id(SENSOR_DOMAIN, DATA_FLAME_ACTIVE), + self._get_entity_id(SENSOR_DOMAIN, DATA_REL_MOD_LEVEL), + self._get_entity_id(SENSOR_DOMAIN, DATA_SLAVE_MEMBERID), + self._get_entity_id(SENSOR_DOMAIN, DATA_BOILER_TEMPERATURE), + self._get_entity_id(SENSOR_DOMAIN, DATA_RETURN_TEMPERATURE), + + self._get_entity_id(SENSOR_DOMAIN, DATA_DHW_SETPOINT_MINIMUM), + self._get_entity_id(SENSOR_DOMAIN, DATA_DHW_SETPOINT_MAXIMUM), + + self._get_entity_id(SWITCH_DOMAIN, DATA_DHW_ENABLE), + self._get_entity_id(SWITCH_DOMAIN, DATA_CENTRAL_HEATING), + + self._get_entity_id(NUMBER_DOMAIN, DATA_DHW_SETPOINT), + self._get_entity_id(NUMBER_DOMAIN, DATA_CONTROL_SETPOINT), + self._get_entity_id(NUMBER_DOMAIN, DATA_MAX_REL_MOD_LEVEL_SETTING), + ])) + + # Track those entities so the coordinator can be updated when something changes + async_track_state_change_event(self.hass, entities, self.async_state_change_event) + + await super().async_added_to_hass(climate) + + async def async_state_change_event(self, _event: Event): + if self._listeners: + self._schedule_refresh() + + self.async_update_listeners() + + async def async_set_control_setpoint(self, value: float) -> None: + await self._send_command_value(DATA_CONTROL_SETPOINT, value) + + await super().async_set_control_setpoint(value) + + async def async_set_control_hot_water_setpoint(self, value: float) -> None: + await self._send_command_value(DATA_DHW_SETPOINT, value) + + await super().async_set_control_hot_water_setpoint(value) + + async def async_set_heater_state(self, state: DeviceState) -> None: + await self._send_command_state(DATA_CENTRAL_HEATING, state == DeviceState.ON) + + await super().async_set_heater_state(state) + + async def async_set_control_max_relative_modulation(self, value: int) -> None: + await self._send_command_value(DATA_MAX_REL_MOD_LEVEL_SETTING, value) + + await super().async_set_control_max_relative_modulation(value) + + async def async_set_control_max_setpoint(self, value: float) -> None: + await self._send_command_value(DATA_DHW_SETPOINT_MAXIMUM, value) + + await super().async_set_control_max_setpoint(value) + + def _get_entity_id(self, domain: str, key: str): + return self._entity_registry.async_get_entity_id(domain, ESPHOME_DOMAIN, f"{self._mac_address}-{key}") + + async def _send_command_value(self, key: str, value): + if not self._simulation: + payload = {"entity_id": self._get_entity_id(NUMBER_DOMAIN, key), value: value} + await self.hass.services.async_call(NUMBER_DOMAIN, SERVICE_SET_VALUE, payload, blocking=True) + + _LOGGER.debug(f"Publishing '{key}':{value} to ESPHome.") + + async def _send_command_state(self, key: str, value: bool): + if not self._simulation: + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + payload = {"entity_id": self._get_entity_id(NUMBER_DOMAIN, key)} + await self.hass.services.async_call(SWITCH_DOMAIN, service, payload, blocking=True) + + _LOGGER.debug(f"Publishing '{key}':{value} to ESPHome.") diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index b64583c0..e2330ef3 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -1,19 +1,18 @@ from __future__ import annotations, annotations import logging -from typing import TYPE_CHECKING, Mapping, Any, Optional +from typing import TYPE_CHECKING, Mapping, Any from homeassistant.components import mqtt from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, Event from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.event import async_track_state_change_event from ..const import * -from ..coordinator import DeviceState, SatDataUpdateCoordinator +from ..coordinator import DeviceState, SatDataUpdateCoordinator, SatEntityCoordinator from ..manufacturers.immergas import Immergas DATA_FLAME_ACTIVE = "flame" @@ -38,7 +37,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) -class SatMqttCoordinator(SatDataUpdateCoordinator): +class SatMqttCoordinator(SatDataUpdateCoordinator, SatEntityCoordinator): """Class to manage to fetch data from the OTGW Gateway using mqtt.""" def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], options: Mapping[str, Any] | None = None) -> None: @@ -232,23 +231,6 @@ async def async_set_control_max_setpoint(self, value: float) -> None: await super().async_set_control_max_setpoint(value) - def get(self, domain: str, key: str) -> Optional[Any]: - """Get the value for the given `key` from the boiler data. - - :param domain: Domain of where this value is located. - :param key: Key of the value to retrieve from the boiler data. - :return: Value for the given key from the boiler data, or None if the boiler data or the value are not available. - """ - entity_id = self._get_entity_id(domain, key) - if entity_id is None: - return None - - state = self.hass.states.get(self._get_entity_id(domain, key)) - if state is None or state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): - return None - - return state.state - def _get_entity_id(self, domain: str, key: str): return self._entity_registry.async_get_entity_id(domain, MQTT_DOMAIN, f"{self._node_id}-{key}") From 8df17097efb18ed82270593f8dcc80e7bb77d304 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 14:53:24 +0200 Subject: [PATCH 055/213] Add esphome to the list --- custom_components/sat/config_flow.py | 1 + custom_components/sat/translations/en.json | 1 + 2 files changed, 2 insertions(+) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 2a0df9bb..f6909a07 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -66,6 +66,7 @@ async def async_step_user(self, _user_input: dict[str, Any] | None = None): if MAJOR_VERSION >= 2023 and (MINOR_VERSION >= 5 or MAJOR_VERSION > 2023): menu_options.append("mosquitto") + menu_options.append("esphome") menu_options.append("serial") menu_options.append("switch") diff --git a/custom_components/sat/translations/en.json b/custom_components/sat/translations/en.json index 0fbe7a1b..62c0236e 100644 --- a/custom_components/sat/translations/en.json +++ b/custom_components/sat/translations/en.json @@ -120,6 +120,7 @@ "menu_options": { "mosquitto": "OpenTherm Gateway ( MQTT )", "serial": "OpenTherm Gateway ( SERIAL )", + "esphome": "OpenTherm Gateway ( ESPHOME )", "simulator": "Simulated Gateway ( ADVANCED )", "switch": "PID Thermostat with PWM ( ON/OFF )" }, From 32fdbbdec955964d692194f76bbaca5a74669293 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 16:16:48 +0200 Subject: [PATCH 056/213] Attempt to get the correct mac address --- custom_components/sat/esphome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index eca9d63f..4bc4a23d 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -50,7 +50,7 @@ def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], self.data = {} self._device = device_registry.async_get(hass).async_get(device_id) - self._mac_address = list(self._device.identifiers)[0][1] + self._mac_address = list(self._device.identifiers)[0] self._entity_registry = entity_registry.async_get(hass) self._entities = entity_registry.async_entries_for_device(self._entity_registry, self._device.id) From ee2c7eea83d416c8b6cd4e3edf0d99ef2e934198 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 16:30:35 +0200 Subject: [PATCH 057/213] Add some debug --- custom_components/sat/esphome/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index 4bc4a23d..400eb248 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -49,7 +49,9 @@ def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], self.data = {} + self._device = device_registry.async_get(hass).async_get(device_id) + _LOGGER.debug(self._device) self._mac_address = list(self._device.identifiers)[0] self._entity_registry = entity_registry.async_get(hass) From 03f635e952001e2c15dcb578de2f9cb505f39792 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 16:44:15 +0200 Subject: [PATCH 058/213] Fix mac address --- custom_components/sat/esphome/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index 400eb248..a76ef76f 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -49,10 +49,8 @@ def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], self.data = {} - self._device = device_registry.async_get(hass).async_get(device_id) - _LOGGER.debug(self._device) - self._mac_address = list(self._device.identifiers)[0] + self._mac_address = list(self._device.connections)[0][1] self._entity_registry = entity_registry.async_get(hass) self._entities = entity_registry.async_entries_for_device(self._entity_registry, self._device.id) From 426bce701950c4c8cd7fac319a28d34e2576155d Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 17:09:39 +0200 Subject: [PATCH 059/213] Add missing domain in the unique entity id --- custom_components/sat/esphome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index a76ef76f..c9fb1e4e 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -204,7 +204,7 @@ async def async_set_control_max_setpoint(self, value: float) -> None: await super().async_set_control_max_setpoint(value) def _get_entity_id(self, domain: str, key: str): - return self._entity_registry.async_get_entity_id(domain, ESPHOME_DOMAIN, f"{self._mac_address}-{key}") + return self._entity_registry.async_get_entity_id(domain, ESPHOME_DOMAIN, f"{self._mac_address}-{domain}-{key}") async def _send_command_value(self, key: str, value): if not self._simulation: From 87b4fbdea05d1e18c2990cf228a31e38dc398373 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 17:20:10 +0200 Subject: [PATCH 060/213] Add device id for future use --- custom_components/sat/coordinator.py | 5 +++++ custom_components/sat/esphome/__init__.py | 6 +++++- custom_components/sat/fake/__init__.py | 4 +++- custom_components/sat/mqtt/__init__.py | 4 ++++ custom_components/sat/serial/__init__.py | 4 ++++ custom_components/sat/simulator/__init__.py | 6 ++++-- custom_components/sat/switch/__init__.py | 6 ++++-- 7 files changed, 29 insertions(+), 6 deletions(-) diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index dace81dc..79848c70 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -76,6 +76,11 @@ def __init__(self, hass: HomeAssistant, data: Mapping[str, Any], options: Mappin super().__init__(hass, _LOGGER, name=DOMAIN) + @property + @abstractmethod + def device_id(self) -> str: + pass + @property def device_state(self): """Return the current state of the device.""" diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index c9fb1e4e..db04306a 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -42,7 +42,7 @@ class SatEspHomeCoordinator(SatDataUpdateCoordinator, SatEntityCoordinator): - """Class to manage to fetch data from the OTGW Gateway using mqtt.""" + """Class to manage to fetch data from the OTGW Gateway using esphome.""" def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], options: Mapping[str, Any] | None = None) -> None: super().__init__(hass, data, options) @@ -55,6 +55,10 @@ def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], self._entity_registry = entity_registry.async_get(hass) self._entities = entity_registry.async_entries_for_device(self._entity_registry, self._device.id) + @property + def device_id(self) -> str: + return self._mac_address + @property def supports_setpoint_management(self): return True diff --git a/custom_components/sat/fake/__init__.py b/custom_components/sat/fake/__init__.py index 316ddfef..fa7e62ba 100644 --- a/custom_components/sat/fake/__init__.py +++ b/custom_components/sat/fake/__init__.py @@ -25,7 +25,9 @@ def __init__( class SatFakeCoordinator(SatDataUpdateCoordinator): - """Class to manage to fetch data from the OTGW Gateway using mqtt.""" + @property + def device_id(self) -> str: + return "Fake" @property def member_id(self) -> int | None: diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index e2330ef3..6717e6a9 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -52,6 +52,10 @@ def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], self._entity_registry = entity_registry.async_get(hass) self._entities = entity_registry.async_entries_for_device(self._entity_registry, self._device.id) + @property + def device_id(self) -> str: + return self._node_id + @property def supports_setpoint_management(self): return True diff --git a/custom_components/sat/serial/__init__.py b/custom_components/sat/serial/__init__.py index c4f608a4..38f45741 100644 --- a/custom_components/sat/serial/__init__.py +++ b/custom_components/sat/serial/__init__.py @@ -41,6 +41,10 @@ async def async_coroutine(event): self._api = OpenThermGateway() self._api.subscribe(async_coroutine) + @property + def device_id(self) -> str: + return self._port + @property def device_active(self) -> bool: return bool(self.get(DATA_MASTER_CH_ENABLED) or False) diff --git a/custom_components/sat/simulator/__init__.py b/custom_components/sat/simulator/__init__.py index eb579242..29521b63 100644 --- a/custom_components/sat/simulator/__init__.py +++ b/custom_components/sat/simulator/__init__.py @@ -14,8 +14,6 @@ class SatSimulatorCoordinator(SatDataUpdateCoordinator): - """Class to manage the Switch.""" - def __init__(self, hass: HomeAssistant, data: Mapping[str, Any], options: Mapping[str, Any] | None = None) -> None: """Initialize.""" super().__init__(hass, data, options) @@ -29,6 +27,10 @@ def __init__(self, hass: HomeAssistant, data: Mapping[str, Any], options: Mappin self._maximum_setpoint = data.get(CONF_MAXIMUM_SETPOINT) self._warming_up = convert_time_str_to_seconds(data.get(CONF_SIMULATED_WARMING_UP)) + @property + def device_id(self) -> str: + return 'Simulator' + @property def supports_setpoint_management(self) -> bool: return True diff --git a/custom_components/sat/switch/__init__.py b/custom_components/sat/switch/__init__.py index a31fb7de..fb5fe67e 100644 --- a/custom_components/sat/switch/__init__.py +++ b/custom_components/sat/switch/__init__.py @@ -17,14 +17,16 @@ class SatSwitchCoordinator(SatDataUpdateCoordinator): - """Class to manage the Switch.""" - def __init__(self, hass: HomeAssistant, entity_id: str, data: Mapping[str, Any], options: Mapping[str, Any] | None = None) -> None: """Initialize.""" super().__init__(hass, data, options) self._entity = entity_registry.async_get(hass).async_get(entity_id) + @property + def device_id(self) -> str: + return self._entity.name + @property def setpoint(self) -> float: return self.minimum_setpoint From 83d38316564482ebd3fae56dc5d843ad7ae1d3ea Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 21:20:01 +0200 Subject: [PATCH 061/213] Add esphome as a supported system --- custom_components/sat/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index f6909a07..cf54b4a6 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -238,7 +238,7 @@ async def async_step_sensors(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: self.data.update(_user_input) - if self.data[CONF_MODE] in [MODE_MQTT, MODE_SERIAL, MODE_SIMULATOR]: + if self.data[CONF_MODE] in [MODE_ESPHOME, MODE_MQTT, MODE_SERIAL, MODE_SIMULATOR]: return await self.async_step_heating_system() return await self.async_step_areas() From bd41fac84344570c46b5a96978f16f76d0256369 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 21:27:35 +0200 Subject: [PATCH 062/213] Fixed typo --- custom_components/sat/esphome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index db04306a..4ffe085a 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -212,7 +212,7 @@ def _get_entity_id(self, domain: str, key: str): async def _send_command_value(self, key: str, value): if not self._simulation: - payload = {"entity_id": self._get_entity_id(NUMBER_DOMAIN, key), value: value} + payload = {"entity_id": self._get_entity_id(NUMBER_DOMAIN, key), "value": value} await self.hass.services.async_call(NUMBER_DOMAIN, SERVICE_SET_VALUE, payload, blocking=True) _LOGGER.debug(f"Publishing '{key}':{value} to ESPHome.") From 746df96b8406ace2b597fdd67a531109b20401b5 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 22:09:26 +0200 Subject: [PATCH 063/213] Typo --- custom_components/sat/esphome/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index 4ffe085a..4bff4cb7 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -31,6 +31,7 @@ DATA_CENTRAL_HEATING = "ch_enabled" # Number +DATA_MAX_CH_SETPOINT = "ch_max_temperature" DATA_DHW_SETPOINT = "dhw_setpoint_temperature" DATA_CONTROL_SETPOINT = "ch_setpoint_temperature" DATA_MAX_REL_MOD_LEVEL_SETTING = "max_modulation" @@ -203,7 +204,7 @@ async def async_set_control_max_relative_modulation(self, value: int) -> None: await super().async_set_control_max_relative_modulation(value) async def async_set_control_max_setpoint(self, value: float) -> None: - await self._send_command_value(DATA_DHW_SETPOINT_MAXIMUM, value) + await self._send_command_value(DATA_MAX_CH_SETPOINT, value) await super().async_set_control_max_setpoint(value) From 1458943368db079f62c3e6bad7c9f0a54a8eae01 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 22:25:02 +0200 Subject: [PATCH 064/213] Improved debugging messages --- custom_components/sat/esphome/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index 4bff4cb7..feae0aa0 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -212,16 +212,20 @@ def _get_entity_id(self, domain: str, key: str): return self._entity_registry.async_get_entity_id(domain, ESPHOME_DOMAIN, f"{self._mac_address}-{domain}-{key}") async def _send_command_value(self, key: str, value): + entity_id = self._get_entity_id(NUMBER_DOMAIN, key) + if not self._simulation: - payload = {"entity_id": self._get_entity_id(NUMBER_DOMAIN, key), "value": value} + payload = {"entity_id": entity_id, "value": value} await self.hass.services.async_call(NUMBER_DOMAIN, SERVICE_SET_VALUE, payload, blocking=True) - _LOGGER.debug(f"Publishing '{key}':{value} to ESPHome.") + _LOGGER.debug(f"Changing number to {value} for '{entity_id}'.") async def _send_command_state(self, key: str, value: bool): + entity_id = self._get_entity_id(NUMBER_DOMAIN, key) + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + if not self._simulation: - service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF payload = {"entity_id": self._get_entity_id(NUMBER_DOMAIN, key)} await self.hass.services.async_call(SWITCH_DOMAIN, service, payload, blocking=True) - _LOGGER.debug(f"Publishing '{key}':{value} to ESPHome.") + _LOGGER.debug(f"Running action {service} for '{entity_id}'.") From c3d7ea788a3085381116376a08d2f2b98e69cc1e Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 22:31:22 +0200 Subject: [PATCH 065/213] More debug messages :) --- custom_components/sat/esphome/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index feae0aa0..bfa435cd 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -209,7 +209,9 @@ async def async_set_control_max_setpoint(self, value: float) -> None: await super().async_set_control_max_setpoint(value) def _get_entity_id(self, domain: str, key: str): - return self._entity_registry.async_get_entity_id(domain, ESPHOME_DOMAIN, f"{self._mac_address}-{domain}-{key}") + unique_id = f"{self._mac_address}-{domain}-{key}" + _LOGGER.debug(f"Attempting to find the unique_id of {unique_id}") + return self._entity_registry.async_get_entity_id(domain, ESPHOME_DOMAIN, unique_id) async def _send_command_value(self, key: str, value): entity_id = self._get_entity_id(NUMBER_DOMAIN, key) From 68b9a95a2b50a11a4eca040c24bf35930cf70d81 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 22:42:33 +0200 Subject: [PATCH 066/213] Cleaned up so code and fixed retrieving state from switches --- custom_components/sat/esphome/__init__.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index bfa435cd..4b5f1e76 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -213,21 +213,20 @@ def _get_entity_id(self, domain: str, key: str): _LOGGER.debug(f"Attempting to find the unique_id of {unique_id}") return self._entity_registry.async_get_entity_id(domain, ESPHOME_DOMAIN, unique_id) - async def _send_command_value(self, key: str, value): - entity_id = self._get_entity_id(NUMBER_DOMAIN, key) - + async def _send_command(self, domain: str, service: str, key: str, payload: dict): + """Helper method to send a command to a specified domain and service.""" if not self._simulation: - payload = {"entity_id": entity_id, "value": value} - await self.hass.services.async_call(NUMBER_DOMAIN, SERVICE_SET_VALUE, payload, blocking=True) + await self.hass.services.async_call(domain, service, payload, blocking=True) - _LOGGER.debug(f"Changing number to {value} for '{entity_id}'.") + _LOGGER.debug(f"Sending '{payload}' to {service} in {domain}.") async def _send_command_state(self, key: str, value: bool): - entity_id = self._get_entity_id(NUMBER_DOMAIN, key) + """Send a command to turn a switch on or off.""" service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + payload = {"entity_id": self._get_entity_id(SWITCH_DOMAIN, key)} + await self._send_command(SWITCH_DOMAIN, service, key, payload) - if not self._simulation: - payload = {"entity_id": self._get_entity_id(NUMBER_DOMAIN, key)} - await self.hass.services.async_call(SWITCH_DOMAIN, service, payload, blocking=True) - - _LOGGER.debug(f"Running action {service} for '{entity_id}'.") + async def _send_command_value(self, key: str, value): + """Send a command to set a numerical value.""" + payload = {"entity_id": self._get_entity_id(NUMBER_DOMAIN, key), "value": value} + await self._send_command(NUMBER_DOMAIN, SERVICE_SET_VALUE, key, payload) From bf3f9e8001ede417ec8603fb030e35fb1dd71de6 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 22:48:09 +0200 Subject: [PATCH 067/213] Fixed the use of domains --- custom_components/sat/esphome/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index 4b5f1e76..364d2e09 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -77,7 +77,7 @@ def supports_relative_modulation_management(self): @property def device_active(self) -> bool: - return self.get(BINARY_SENSOR_DOMAIN, DATA_CENTRAL_HEATING) == DeviceState.ON + return self.get(SWITCH_DOMAIN, DATA_CENTRAL_HEATING) == DeviceState.ON @property def flame_active(self) -> bool: @@ -89,14 +89,14 @@ def hot_water_active(self) -> bool: @property def setpoint(self) -> float | None: - if (setpoint := self.get(SENSOR_DOMAIN, DATA_CONTROL_SETPOINT)) is not None: + if (setpoint := self.get(NUMBER_DOMAIN, DATA_CONTROL_SETPOINT)) is not None: return float(setpoint) return None @property def hot_water_setpoint(self) -> float | None: - if (setpoint := self.get(SENSOR_DOMAIN, DATA_DHW_SETPOINT)) is not None: + if (setpoint := self.get(NUMBER_DOMAIN, DATA_DHW_SETPOINT)) is not None: return float(setpoint) return super().hot_water_setpoint From a597321ec38714895057ed952b6dcf1813313a0e Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Sep 2024 23:10:52 +0200 Subject: [PATCH 068/213] Attempting to fix the unique id --- custom_components/sat/esphome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index 364d2e09..c349e0dc 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -209,7 +209,7 @@ async def async_set_control_max_setpoint(self, value: float) -> None: await super().async_set_control_max_setpoint(value) def _get_entity_id(self, domain: str, key: str): - unique_id = f"{self._mac_address}-{domain}-{key}" + unique_id = f"{self._mac_address.upper()}-{domain}-{key}" _LOGGER.debug(f"Attempting to find the unique_id of {unique_id}") return self._entity_registry.async_get_entity_id(domain, ESPHOME_DOMAIN, unique_id) From a1e4e9a3ccf1bdfcfc5f8d7a27d05bb3a754f830 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Mon, 9 Sep 2024 00:07:54 +0200 Subject: [PATCH 069/213] Hopefully fixed modulation (dev did not follow naming) --- custom_components/sat/esphome/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index c349e0dc..9bd19642 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -34,7 +34,7 @@ DATA_MAX_CH_SETPOINT = "ch_max_temperature" DATA_DHW_SETPOINT = "dhw_setpoint_temperature" DATA_CONTROL_SETPOINT = "ch_setpoint_temperature" -DATA_MAX_REL_MOD_LEVEL_SETTING = "max_modulation" +DATA_MAX_REL_MOD_LEVEL_SETTING = "max_modulation_level" if TYPE_CHECKING: from ..climate import SatClimate @@ -226,7 +226,7 @@ async def _send_command_state(self, key: str, value: bool): payload = {"entity_id": self._get_entity_id(SWITCH_DOMAIN, key)} await self._send_command(SWITCH_DOMAIN, service, key, payload) - async def _send_command_value(self, key: str, value): + async def _send_command_value(self, key: str, value: float): """Send a command to set a numerical value.""" payload = {"entity_id": self._get_entity_id(NUMBER_DOMAIN, key), "value": value} await self._send_command(NUMBER_DOMAIN, SERVICE_SET_VALUE, key, payload) From e7d760b049304ef9b7ba460ea53d7b356ddca7eb Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Mon, 9 Sep 2024 00:17:08 +0200 Subject: [PATCH 070/213] Hopefully fixed max ch temperature (dev did not follow naming) --- custom_components/sat/esphome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index 9bd19642..6f49c1c2 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -31,9 +31,9 @@ DATA_CENTRAL_HEATING = "ch_enabled" # Number -DATA_MAX_CH_SETPOINT = "ch_max_temperature" DATA_DHW_SETPOINT = "dhw_setpoint_temperature" DATA_CONTROL_SETPOINT = "ch_setpoint_temperature" +DATA_MAX_CH_SETPOINT = "max_ch_setpoint_temperature" DATA_MAX_REL_MOD_LEVEL_SETTING = "max_modulation_level" if TYPE_CHECKING: From dd857fb6deaafe3ee16d5a87dd50a93d39569de1 Mon Sep 17 00:00:00 2001 From: Conrad Juhl Andersen Date: Wed, 11 Sep 2024 15:55:54 +0200 Subject: [PATCH 071/213] Added geminox manufacturer --- custom_components/sat/manufacturer.py | 4 ++++ custom_components/sat/manufacturers/geminox.py | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 custom_components/sat/manufacturers/geminox.py diff --git a/custom_components/sat/manufacturer.py b/custom_components/sat/manufacturer.py index fbf5a627..e49bd449 100644 --- a/custom_components/sat/manufacturer.py +++ b/custom_components/sat/manufacturer.py @@ -14,6 +14,10 @@ def resolve(self, member_id: int) -> Manufacturer | None: if member_id == -1: from custom_components.sat.manufacturers.simulator import Simulator return Simulator() + + if member_id == 4: + from custom_components.sat.manufacturers.geminox import Geminox + return Geminox() if member_id == 6: from custom_components.sat.manufacturers.ideal import Ideal diff --git a/custom_components/sat/manufacturers/geminox.py b/custom_components/sat/manufacturers/geminox.py new file mode 100644 index 00000000..08be39ec --- /dev/null +++ b/custom_components/sat/manufacturers/geminox.py @@ -0,0 +1,7 @@ +from custom_components.sat.manufacturer import Manufacturer + + +class Geminox(Manufacturer): + @property + def name(self) -> str: + return 'Geminox' From c7ebd459c9beaceaa280d7c68b28a1bd52843e7b Mon Sep 17 00:00:00 2001 From: Conrad Juhl Andersen Date: Wed, 11 Sep 2024 15:56:22 +0200 Subject: [PATCH 072/213] Added missing esphome sensors --- custom_components/sat/esphome/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index 6f49c1c2..bc3d33a2 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -22,6 +22,8 @@ DATA_SLAVE_MEMBERID = "boiler_member_id" DATA_BOILER_TEMPERATURE = "boiler_temperature" DATA_RETURN_TEMPERATURE = "return_temperature" +DATA_BOILER_CAPACITY = "max_capacity" +DATA_REL_MIN_MOD_LEVEL = "min_mod_level" DATA_DHW_SETPOINT_MINIMUM = "dhw_min_temperature" DATA_DHW_SETPOINT_MAXIMUM = "dhw_max_temperature" @@ -135,6 +137,20 @@ def relative_modulation_value(self) -> float | None: return float(value) return super().relative_modulation_value + + @property + def boiler_capacity(self) -> float | None: + if (value := self.get(SENSOR_DOMAIN, DATA_BOILER_CAPACITY)) is not None: + return float(value) + + return super().boiler_capacity + + @property + def minimum_relative_modulation_value(self) -> float | None: + if (value := self.get(SENSOR_DOMAIN, DATA_REL_MIN_MOD_LEVEL)) is not None: + return float(value) + + return super().minimum_relative_modulation_value @property def maximum_relative_modulation_value(self) -> float | None: From e9b953a9dc719770e1b7f909f351f5b7b07b85d0 Mon Sep 17 00:00:00 2001 From: Rui Melo Date: Thu, 19 Sep 2024 14:06:03 +0100 Subject: [PATCH 073/213] Create pt.json --- custom_components/sat/translations/pt.json | 77 ++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 custom_components/sat/translations/pt.json diff --git a/custom_components/sat/translations/pt.json b/custom_components/sat/translations/pt.json new file mode 100644 index 00000000..35ece027 --- /dev/null +++ b/custom_components/sat/translations/pt.json @@ -0,0 +1,77 @@ +{ + "config": { + "abort": { + "already_configured": "O Gateway já está configurado.", + "reconfigure_successful": "O Gateway foi reconfigurado com sucesso." + }, + "error": { + "connection": "Não foi possível conectar ao gateway.", + "mqtt_component": "O componente MQTT está indisponível.", + "unable_to_calibrate": "O processo de calibração encontrou um problema e não pôde ser concluído com sucesso. Verifique se o seu sistema de aquecimento está funcionando corretamente e se todos os sensores necessários estão conectados e funcionando corretamente.\n\nSe você continuar a ter problemas com a calibração, considere entrar em contato conosco para obter assistência. Pedimos desculpas por qualquer inconveniente causado." + }, + "progress": { + "calibration": "Calibrando e encontrando o valor de proteção contra superaquecimento...\n\nAguarde enquanto otimizamos seu sistema de aquecimento. Este processo pode levar cerca de 20 minutos." + }, + "step": { + "areas": { + "data": { + "main_climates": "Principal", + "secondary_climates": "Cômodos" + }, + "description": "Configurações relacionadas aos climas, controle de múltiplos cômodos e controle de temperatura. Climas principais estão no mesmo cômodo que o sensor interno, enquanto os cômodos têm suas próprias temperaturas-alvo, separadas do sistema.", + "title": "Áreas" + }, + "automatic_gains": { + "data": { + "automatic_gains": "Ganho Automático (recomendado)" + }, + "description": "Este recurso ajusta dinamicamente os parâmetros de controle do seu sistema de aquecimento, otimizando o controle de temperatura para maior conforto e eficiência energética. Ativar esta opção permite que o SAT adapte e ajuste continuamente as configurações de aquecimento com base nas condições ambientais. Isso ajuda a manter um ambiente estável e confortável sem a necessidade de intervenção manual.\n\nNota: Se você optar por não ativar os ganhos automáticos, será necessário inserir manualmente os valores PID para um controle de temperatura preciso. Certifique-se de ter valores PID precisos para o seu sistema de aquecimento específico para obter o melhor desempenho.", + "title": "Ganho Automático" + }, + "calibrate_system": { + "description": "Otimize seu sistema de aquecimento determinando automaticamente os valores PID ideais para sua configuração. Ao selecionar o Ganho Automático, note que o sistema passará por um processo de calibração que pode levar cerca de 20 minutos para ser concluído.\n\nO Ganho Automático é recomendado para a maioria dos usuários, pois simplifica o processo de configuração e garante o melhor desempenho. No entanto, se você estiver familiarizado com o controle PID e preferir definir os valores manualmente, pode optar por pular o Ganho Automático.\n\nObserve que escolher pular o Ganho Automático exige um bom entendimento de controle PID e pode exigir ajustes manuais adicionais para alcançar o desempenho ideal.", + "menu_options": { + "calibrate": "Calibrar e determinar o valor de proteção contra superaquecimento (aprox. 20 min).", + "overshoot_protection": "Inserir manualmente o valor de proteção contra superaquecimento.", + "pid_controller": "Inserir manualmente os valores PID (não recomendado)." + }, + "title": "Calibrar Sistema" + }, + "calibrated": { + "description": "O processo de calibração foi concluído com sucesso.\n\nParabéns! Seu Termostato Autotune Inteligente (SAT) foi calibrado para otimizar o desempenho de aquecimento do seu sistema. Durante o processo de calibração, o SAT analisou cuidadosamente as características do aquecimento e determinou o valor apropriado de proteção contra superaquecimento para garantir um controle preciso da temperatura.\n\nValor de Proteção contra Superaquecimento: {minimum_setpoint} °C\n\nEste valor representa a quantidade máxima de superaquecimento permitida durante o processo de aquecimento. O SAT monitorará ativamente e ajustará o aquecimento para evitar superaquecimento excessivo, mantendo uma experiência de aquecimento confortável e eficiente em sua casa.\n\nObserve que o valor de proteção contra superaquecimento pode variar dependendo das características específicas do seu sistema de aquecimento e dos fatores ambientais. Ele foi ajustado para fornecer o melhor desempenho com base nos resultados da calibração.", + "menu_options": { + "calibrate": "Tentar calibração novamente", + "finish": "Continuar com a calibração atual" + }, + "title": "Calibração Concluída" + }, + "heating_system": { + "data": { + "heating_system": "Sistema" + }, + "description": "Selecionar o tipo correto de sistema de aquecimento é importante para que o SAT controle a temperatura com precisão e otimize o desempenho. Escolha a opção que corresponde à sua configuração para garantir uma regulação adequada da temperatura em toda a sua casa.", + "title": "Sistema de Aquecimento" + }, + "mosquitto": { + "data": { + "device": "Dispositivo", + "mqtt_topic": "Tópico MQTT", + "name": "Nome" + }, + "description": "Por favor, forneça as seguintes informações para configurar o Gateway OpenTherm. No campo Nome, insira um nome para o gateway que o ajudará a identificá-lo dentro do seu sistema.\n\nEspecifique a entidade de clima a ser usada para o Gateway OpenTherm. Esta entidade é fornecida pelo Gateway OpenTherm e representa o seu sistema de aquecimento.\n\nAlém disso, insira o Tópico MQTT que será usado para publicar e subscrever mensagens relacionadas ao Gateway OpenTherm.\n\nEssas configurações são essenciais para estabelecer a comunicação e integração com o seu Gateway OpenTherm por meio do MQTT. Elas permitem a troca de dados e o controle do seu sistema de aquecimento. Certifique-se de que os detalhes fornecidos sejam precisos para garantir a funcionalidade correta.", + "title": "Gateway OpenTherm (MQTT)" + }, + "overshoot_protection": { + "data": { + "minimum_setpoint": "Valor" + }, + "description": "Ao fornecer o valor de proteção contra superaquecimento, o SAT ajustará os parâmetros de controle de acordo para manter um ambiente de aquecimento estável e confortável. Esta configuração manual permite ajustar o sistema com base na sua configuração específica.\n\nNota: Se você não tiver certeza sobre o valor de proteção contra superaquecimento ou não tiver realizado o processo de calibração, é recomendável cancelar a configuração e realizar o processo de calibração para que o SAT determine automaticamente o valor para o melhor desempenho.", + "title": "Proteção contra Superaquecimento" + }, + "pid_controller": { + "data": { + "derivative": "Derivativo (kD)", + "integral": "Integral (kI)", + "proportional": "Proporcional (kP)" + }, + "description": "Configure manualmente os ganhos proporcional, integral e derivativo para ajustar seu sistema de aquecimento. Use esta opção se pre From 3a16164188bc7947bf8d80ca73db19841e88c520 Mon Sep 17 00:00:00 2001 From: Rui Melo Date: Thu, 19 Sep 2024 16:51:07 +0100 Subject: [PATCH 074/213] missing part Update pt.json --- custom_components/sat/translations/pt.json | 196 ++++++++++++++++++--- 1 file changed, 174 insertions(+), 22 deletions(-) diff --git a/custom_components/sat/translations/pt.json b/custom_components/sat/translations/pt.json index 35ece027..b776ae12 100644 --- a/custom_components/sat/translations/pt.json +++ b/custom_components/sat/translations/pt.json @@ -5,42 +5,42 @@ "reconfigure_successful": "O Gateway foi reconfigurado com sucesso." }, "error": { - "connection": "Não foi possível conectar ao gateway.", - "mqtt_component": "O componente MQTT está indisponível.", - "unable_to_calibrate": "O processo de calibração encontrou um problema e não pôde ser concluído com sucesso. Verifique se o seu sistema de aquecimento está funcionando corretamente e se todos os sensores necessários estão conectados e funcionando corretamente.\n\nSe você continuar a ter problemas com a calibração, considere entrar em contato conosco para obter assistência. Pedimos desculpas por qualquer inconveniente causado." + "connection": "Não foi possível ligar ao gateway.", + "mqtt_component": "O componente MQTT não está disponível.", + "unable_to_calibrate": "O processo de calibração encontrou um problema e não pôde ser concluído com sucesso. Por favor, assegure-se de que o seu sistema de aquecimento está a funcionar corretamente e que todos os sensores necessários estão ligados e a funcionar corretamente.\n\nSe continuar a ter problemas com a calibração, considere contactar-nos para obter mais assistência. Pedimos desculpa por qualquer inconveniente causado." }, "progress": { - "calibration": "Calibrando e encontrando o valor de proteção contra superaquecimento...\n\nAguarde enquanto otimizamos seu sistema de aquecimento. Este processo pode levar cerca de 20 minutos." + "calibration": "A calibrar e a encontrar o valor de proteção contra overshoot...\n\nPor favor, aguarde enquanto otimizamos o seu sistema de aquecimento. Este processo pode demorar aproximadamente 20 minutos." }, "step": { "areas": { "data": { "main_climates": "Principal", - "secondary_climates": "Cômodos" + "secondary_climates": "Quartos" }, - "description": "Configurações relacionadas aos climas, controle de múltiplos cômodos e controle de temperatura. Climas principais estão no mesmo cômodo que o sensor interno, enquanto os cômodos têm suas próprias temperaturas-alvo, separadas do sistema.", + "description": "Definições relacionadas com climas, multi-sala e controlo de temperatura. Climas principais estão na mesma sala que o sensor interior e os quartos têm as suas próprias temperaturas alvo separadas do sistema.", "title": "Áreas" }, "automatic_gains": { "data": { - "automatic_gains": "Ganho Automático (recomendado)" + "automatic_gains": "Ganhos Automáticos (recomendado)" }, - "description": "Este recurso ajusta dinamicamente os parâmetros de controle do seu sistema de aquecimento, otimizando o controle de temperatura para maior conforto e eficiência energética. Ativar esta opção permite que o SAT adapte e ajuste continuamente as configurações de aquecimento com base nas condições ambientais. Isso ajuda a manter um ambiente estável e confortável sem a necessidade de intervenção manual.\n\nNota: Se você optar por não ativar os ganhos automáticos, será necessário inserir manualmente os valores PID para um controle de temperatura preciso. Certifique-se de ter valores PID precisos para o seu sistema de aquecimento específico para obter o melhor desempenho.", - "title": "Ganho Automático" + "description": "Esta funcionalidade ajusta os parâmetros de controlo do seu sistema de aquecimento dinamicamente, otimizando o controlo de temperatura para melhor conforto e eficiência energética. Ao ativar esta opção, o SAT adapta-se continuamente e afina as definições de aquecimento com base nas condições ambientais. Isto ajuda a manter um ambiente estável e confortável sem intervenção manual.\n\nNota: Se optar por não ativar os ganhos automáticos, terá de introduzir manualmente os valores PID para controlo preciso da temperatura. Por favor, certifique-se de que tem valores PID precisos para o seu sistema de aquecimento específico para obter desempenho ideal.", + "title": "Ganhos Automáticos" }, "calibrate_system": { - "description": "Otimize seu sistema de aquecimento determinando automaticamente os valores PID ideais para sua configuração. Ao selecionar o Ganho Automático, note que o sistema passará por um processo de calibração que pode levar cerca de 20 minutos para ser concluído.\n\nO Ganho Automático é recomendado para a maioria dos usuários, pois simplifica o processo de configuração e garante o melhor desempenho. No entanto, se você estiver familiarizado com o controle PID e preferir definir os valores manualmente, pode optar por pular o Ganho Automático.\n\nObserve que escolher pular o Ganho Automático exige um bom entendimento de controle PID e pode exigir ajustes manuais adicionais para alcançar o desempenho ideal.", + "description": "Otimize o seu sistema de aquecimento determinando automaticamente os valores PID ótimos para a sua configuração. Ao selecionar Ganhos Automáticos, por favor note que o sistema passará por um processo de calibração que pode demorar aproximadamente 20 minutos a completar.\n\nGanhos Automáticos é recomendado para a maioria dos utilizadores, pois simplifica o processo de configuração e assegura desempenho ideal. No entanto, se estiver familiarizado com controlo PID e preferir definir os valores manualmente, pode optar por ignorar os Ganhos Automáticos.\n\nPor favor note que escolher ignorar os Ganhos Automáticos requer um bom entendimento de controlo PID e pode requerer ajustes manuais adicionais para alcançar desempenho ótimo.", "menu_options": { - "calibrate": "Calibrar e determinar o valor de proteção contra superaquecimento (aprox. 20 min).", - "overshoot_protection": "Inserir manualmente o valor de proteção contra superaquecimento.", - "pid_controller": "Inserir manualmente os valores PID (não recomendado)." + "calibrate": "Calibrar e determinar o valor de proteção contra overshoot (aprox. 20 min).", + "overshoot_protection": "Introduzir manualmente o valor de proteção contra overshoot.", + "pid_controller": "Introduzir manualmente os valores PID (não recomendado)." }, "title": "Calibrar Sistema" }, "calibrated": { - "description": "O processo de calibração foi concluído com sucesso.\n\nParabéns! Seu Termostato Autotune Inteligente (SAT) foi calibrado para otimizar o desempenho de aquecimento do seu sistema. Durante o processo de calibração, o SAT analisou cuidadosamente as características do aquecimento e determinou o valor apropriado de proteção contra superaquecimento para garantir um controle preciso da temperatura.\n\nValor de Proteção contra Superaquecimento: {minimum_setpoint} °C\n\nEste valor representa a quantidade máxima de superaquecimento permitida durante o processo de aquecimento. O SAT monitorará ativamente e ajustará o aquecimento para evitar superaquecimento excessivo, mantendo uma experiência de aquecimento confortável e eficiente em sua casa.\n\nObserve que o valor de proteção contra superaquecimento pode variar dependendo das características específicas do seu sistema de aquecimento e dos fatores ambientais. Ele foi ajustado para fornecer o melhor desempenho com base nos resultados da calibração.", + "description": "O processo de calibração foi concluído com sucesso.\n\nParabéns! O seu Termóstato Smart Autotune (SAT) foi calibrado para otimizar o desempenho de aquecimento do seu sistema. Durante o processo de calibração, o SAT analisou cuidadosamente as características de aquecimento e determinou o valor apropriado de proteção contra overshoot para assegurar controlo de temperatura preciso.\n\nValor de Proteção contra Overshoot: {minimum_setpoint} °C\n\nEste valor representa a quantidade máxima de overshoot permitida durante o processo de aquecimento. O SAT irá monitorizar ativamente e ajustar o aquecimento para prevenir overshoot excessivo, mantendo uma experiência de aquecimento confortável e eficiente na sua casa.\n\nPor favor note que o valor de proteção contra overshoot pode variar dependendo das características específicas do seu sistema de aquecimento e fatores ambientais. Foi afinado para fornecer desempenho ótimo com base nos resultados da calibração.", "menu_options": { - "calibrate": "Tentar calibração novamente", + "calibrate": "Repetir calibração", "finish": "Continuar com a calibração atual" }, "title": "Calibração Concluída" @@ -49,24 +49,24 @@ "data": { "heating_system": "Sistema" }, - "description": "Selecionar o tipo correto de sistema de aquecimento é importante para que o SAT controle a temperatura com precisão e otimize o desempenho. Escolha a opção que corresponde à sua configuração para garantir uma regulação adequada da temperatura em toda a sua casa.", + "description": "Selecionar o tipo correto de sistema de aquecimento é importante para que o SAT controle com precisão a temperatura e otimize o desempenho. Escolha a opção que corresponde à sua configuração para assegurar regulação adequada da temperatura em toda a sua casa.", "title": "Sistema de Aquecimento" }, "mosquitto": { "data": { "device": "Dispositivo", - "mqtt_topic": "Tópico MQTT", + "mqtt_topic": "Tópico Principal", "name": "Nome" }, - "description": "Por favor, forneça as seguintes informações para configurar o Gateway OpenTherm. No campo Nome, insira um nome para o gateway que o ajudará a identificá-lo dentro do seu sistema.\n\nEspecifique a entidade de clima a ser usada para o Gateway OpenTherm. Esta entidade é fornecida pelo Gateway OpenTherm e representa o seu sistema de aquecimento.\n\nAlém disso, insira o Tópico MQTT que será usado para publicar e subscrever mensagens relacionadas ao Gateway OpenTherm.\n\nEssas configurações são essenciais para estabelecer a comunicação e integração com o seu Gateway OpenTherm por meio do MQTT. Elas permitem a troca de dados e o controle do seu sistema de aquecimento. Certifique-se de que os detalhes fornecidos sejam precisos para garantir a funcionalidade correta.", - "title": "Gateway OpenTherm (MQTT)" + "description": "Por favor forneça os seguintes detalhes para configurar o OpenTherm Gateway. No campo Nome, insira um nome para o gateway que o ajudará a identificá-lo dentro do seu sistema.\n\nEspecifique a entidade Climate a utilizar para o OpenTherm Gateway. Esta entidade é fornecida pelo OpenTherm Gateway e representa o seu sistema de aquecimento.\n\nAdicionalmente, insira o Tópico Principal que será usado para publicar e subscrever mensagens MQTT relacionadas com o OpenTherm Gateway.\n\nEstas definições são essenciais para estabelecer comunicação e integração com o seu OpenTherm Gateway através do MQTT. Elas permitem troca de dados e controlo do seu sistema de aquecimento de forma fluida. Assegure-se de que os detalhes fornecidos são precisos para garantir funcionalidade adequada.", + "title": "OpenTherm Gateway ( MQTT )" }, "overshoot_protection": { "data": { "minimum_setpoint": "Valor" }, - "description": "Ao fornecer o valor de proteção contra superaquecimento, o SAT ajustará os parâmetros de controle de acordo para manter um ambiente de aquecimento estável e confortável. Esta configuração manual permite ajustar o sistema com base na sua configuração específica.\n\nNota: Se você não tiver certeza sobre o valor de proteção contra superaquecimento ou não tiver realizado o processo de calibração, é recomendável cancelar a configuração e realizar o processo de calibração para que o SAT determine automaticamente o valor para o melhor desempenho.", - "title": "Proteção contra Superaquecimento" + "description": "Ao fornecer o valor de proteção contra overshoot, o SAT ajustará os parâmetros de controlo em conformidade para manter um ambiente de aquecimento confortável e estável. Esta configuração manual permite-lhe afinar o sistema com base na sua configuração específica.\n\nNota: Se não tiver certeza sobre o valor de proteção contra overshoot ou não tiver realizado o processo de calibração, é recomendado cancelar a configuração e passar pelo processo de calibração para permitir que o SAT determine automaticamente o valor para desempenho ótimo.", + "title": "Proteção contra Overshoot" }, "pid_controller": { "data": { @@ -74,4 +74,156 @@ "integral": "Integral (kI)", "proportional": "Proporcional (kP)" }, - "description": "Configure manualmente os ganhos proporcional, integral e derivativo para ajustar seu sistema de aquecimento. Use esta opção se pre + "description": "Configure os ganhos proporcional, integral e derivativo manualmente para afinar o seu sistema de aquecimento. Use esta opção se preferir ter controlo total sobre os parâmetros do controlador PID. Ajuste os ganhos com base nas características e preferências específicas do seu sistema de aquecimento.", + "title": "Configurar o controlador PID manualmente." + }, + "sensors": { + "data": { + "humidity_sensor_entity_id": "Entidade do Sensor de Humidade", + "inside_sensor_entity_id": "Entidade do Sensor Interior", + "outside_sensor_entity_id": "Entidade do Sensor Exterior" + }, + "description": "Por favor selecione os sensores que serão usados para monitorizar a temperatura.", + "title": "Configurar sensores" + }, + "serial": { + "data": { + "device": "URL", + "name": "Nome" + }, + "description": "Para estabelecer uma ligação com o OpenTherm Gateway usando uma ligação socket, por favor forneça os seguintes detalhes. No campo Nome, insira um nome para o gateway que o ajudará a identificá-lo dentro do seu sistema.\n\nEspecifique o endereço de rede do OpenTherm Gateway no campo Dispositivo. Isto pode ser no formato \"socket://otgw.local:25238\", onde \"otgw.local\" é o nome de host ou endereço IP do gateway e \"25238\" é o número da porta.\n\nEstas definições são essenciais para estabelecer comunicação e integração com o seu OpenTherm Gateway através da ligação socket. Assegure-se de que os detalhes fornecidos são precisos para garantir funcionalidade adequada.", + "title": "OpenTherm Gateway ( SERIAL )" + }, + "simulator": { + "data": { + "maximum_setpoint": "Setpoint Máximo", + "minimum_setpoint": "Setpoint Mínimo", + "name": "Nome", + "simulated_cooling": "Arrefecimento Simulado", + "simulated_heating": "Aquecimento Simulado", + "simulated_warming_up": "Aquecimento Simulado" + }, + "description": "Este gateway permite-lhe simular uma caldeira para fins de teste e demonstração. Por favor forneça as seguintes informações para configurar o simulador.\n\nNota: O Simulador Gateway destina-se apenas a fins de teste e demonstração e não deve ser usado em ambientes de produção.", + "title": "Gateway Simulado ( AVANÇADO )" + }, + "switch": { + "data": { + "device": "Entidade", + "minimum_setpoint": "Definição de Temperatura", + "name": "Nome" + }, + "description": "Por favor preencha os seguintes detalhes para configurar o interruptor. Insira um nome para o interruptor no campo Nome, que o ajudará a identificá-lo dentro do seu sistema. Escolha a entidade apropriada a utilizar para o seu interruptor das opções fornecidas.\n\nNo campo Definição de Temperatura, especifique a temperatura alvo desejada para o seu sistema de aquecimento. Se estiver a usar uma caldeira de água quente, preencha a Definição de Temperatura da Caldeira com o valor apropriado. Para sistemas de aquecimento elétrico, insira o valor 100.\n\nEstas definições são essenciais para controlo preciso da temperatura e assegurar desempenho ideal do seu sistema de aquecimento. Fornecer a Definição de Temperatura correta permite regulação precisa e ajuda a alcançar um ambiente confortável e energeticamente eficiente na sua casa.", + "title": "Termóstato PID com PWM ( LIGA/DESLIGA )" + }, + "user": { + "description": "O SAT é um termóstato inteligente capaz de auto-afinar-se para otimizar o controlo de temperatura. Selecione o modo apropriado que corresponde ao seu sistema de aquecimento.", + "menu_options": { + "mosquitto": "OpenTherm Gateway ( MQTT )", + "serial": "OpenTherm Gateway ( SERIAL )", + "esphome": "OpenTherm Gateway ( ESPHOME )", + "simulator": "Gateway Simulado ( AVANÇADO )", + "switch": "Termóstato PID com PWM ( LIGA/DESLIGA )" + }, + "title": "Termóstato Smart Autotune (SAT)" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "climate_valve_offset": "Offset da válvula do clima", + "dynamic_minimum_setpoint": "Setpoint Mínimo Dinâmico (Experimental)", + "force_pulse_width_modulation": "Forçar Modulação por Largura de Pulso", + "maximum_consumption": "Consumo Máximo", + "maximum_relative_modulation": "Modulação Relativa Máxima", + "minimum_consumption": "Consumo Mínimo", + "sample_time": "Tempo de Amostragem", + "simulation": "Simulação", + "target_temperature_step": "Passo da Temperatura Alvo", + "thermal_comfort": "Conforto Térmico" + }, + "data_description": { + "climate_valve_offset": "Offset para ajustar o grau de abertura da válvula do clima.", + "dynamic_minimum_setpoint": "Ativa o ajuste dinâmico do setpoint com base na temperatura de retorno da caldeira, o que também ajuda a identificar se alguma válvula está fechada.", + "maximum_consumption": "O consumo máximo de gás quando a caldeira está ativa.", + "maximum_relative_modulation": "Representa o nível máximo de modulação para um sistema de aquecimento eficiente.", + "minimum_consumption": "O consumo mínimo de gás quando a caldeira está ativa.", + "sample_time": "O intervalo de tempo mínimo entre atualizações do controlador PID.", + "target_temperature_step": "Ajuste o passo da temperatura alvo para afinação do nível de conforto.", + "thermal_comfort": "Ativar o uso do Índice de Simmer para ajuste do conforto térmico." + }, + "title": "Avançado" + }, + "general": { + "data": { + "automatic_gains_value": "Valor de Ganhos Automáticos", + "derivative": "Derivativo (kD)", + "derivative_time_weight": "Peso Temporal do Derivativo", + "duty_cycle": "Ciclo de Trabalho Máximo para Modulação por Largura de Pulso", + "heating_curve_coefficient": "Coeficiente da Curva de Aquecimento", + "heating_curve_version": "Versão da Curva de Aquecimento", + "integral": "Integral (kI)", + "maximum_setpoint": "Setpoint Máximo", + "minimum_setpoint_adjustment_factor": "Fator de Ajuste para Temperatura de Retorno", + "pid_controller_version": "Versão do Controlador PID", + "proportional": "Proporcional (kP)", + "sync_with_thermostat": "Sincronizar setpoint com termóstato", + "window_sensors": "Sensores de Contato", + "heating_mode": "Modo de Aquecimento" + }, + "data_description": { + "automatic_gains_value": "O valor usado para ganhos automáticos no controlador PID.", + "derivative": "O termo derivativo (kD) no controlador PID, responsável por mitigar overshooting.", + "derivative_time_weight": "Um parâmetro para ajustar a influência do termo derivativo ao longo do tempo, particularmente útil para reduzir undershoot durante a fase de aquecimento quando o coeficiente da curva de aquecimento está corretamente definido.", + "duty_cycle": "O ciclo de trabalho máximo para Modulação por Largura de Pulso (PWM), controlando os ciclos de ligar/desligar da caldeira.", + "heating_curve_coefficient": "O coeficiente usado para ajustar a curva de aquecimento.", + "integral": "O termo integral (kI) no controlador PID, responsável por reduzir o erro em regime permanente.", + "maximum_setpoint": "A temperatura ótima para operação eficiente da caldeira.", + "minimum_setpoint_adjustment_factor": "Este fator ajusta o setpoint de aquecimento com base na temperatura de retorno da caldeira, afetando a resposta e eficiência do aquecimento. Um valor mais alto aumenta a sensibilidade às mudanças de temperatura, melhorando o controlo sobre o conforto e uso de energia. Faixa inicial recomendada é de 0.1 a 0.5. Ajuste para adequar ao seu sistema e preferências de conforto.", + "proportional": "O termo proporcional (kP) no controlador PID, responsável pela resposta imediata a erros.", + "sync_with_thermostat": "Sincronizar setpoint com termóstato para assegurar controlo de temperatura coordenado.", + "window_sensors": "Sensores de Contato que ativam o sistema para reagir quando uma janela ou porta está aberta por um período de tempo." + }, + "description": "Definições e configurações gerais.", + "title": "Geral" + }, + "init": { + "menu_options": { + "advanced": "Opções Avançadas", + "general": "Geral", + "presets": "Predefinições", + "system_configuration": "Configuração do Sistema" + } + }, + "presets": { + "data": { + "activity_temperature": "Temperatura de Atividade", + "away_temperature": "Temperatura Ausente", + "comfort_temperature": "Temperatura de Conforto", + "home_temperature": "Temperatura em Casa", + "sleep_temperature": "Temperatura de Sono", + "sync_climates_with_preset": "Sincronizar climas com predefinição (sono / ausente / atividade)" + }, + "description": "Definições de temperatura predefinidas para diferentes cenários ou atividades.", + "title": "Predefinições" + }, + "system_configuration": { + "data": { + "automatic_duty_cycle": "Ciclo de trabalho automático", + "overshoot_protection": "Proteção contra Overshoot (com PWM)", + "sensor_max_value_age": "Idade máxima do valor do sensor de temperatura", + "window_minimum_open_time": "Tempo mínimo para a janela estar aberta" + }, + "data_description": { + "automatic_duty_cycle": "Ativar ou desativar ciclo de trabalho automático para Modulação por Largura de Pulso (PWM).", + "overshoot_protection": "Ativar proteção contra overshoot com Modulação por Largura de Pulso (PWM) para evitar overshooting da temperatura da caldeira.", + "sensor_max_value_age": "A idade máxima do valor do sensor de temperatura antes de ser considerado como estagnado.", + "window_minimum_open_time": "O tempo mínimo que uma janela deve estar aberta antes de o sistema reagir." + }, + "description": "Para afinação e personalização.", + "title": "Configuração do Sistema" + } + } + } +} From 144aa9f5cf7d1053d1b3010a2286fd68ca102c76 Mon Sep 17 00:00:00 2001 From: misa1515 <61636045+misa1515@users.noreply.github.com> Date: Tue, 15 Oct 2024 08:46:14 +0200 Subject: [PATCH 075/213] Create sk.json --- custom_components/sat/translations/sk.json | 229 +++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 custom_components/sat/translations/sk.json diff --git a/custom_components/sat/translations/sk.json b/custom_components/sat/translations/sk.json new file mode 100644 index 00000000..2b4a6426 --- /dev/null +++ b/custom_components/sat/translations/sk.json @@ -0,0 +1,229 @@ +{ + "config": { + "abort": { + "already_configured": "Gateway je už nakonfigurovaný.", + "reconfigure_successful": "Gateway bol prekonfigurovaný." + }, + "error": { + "connection": "Nedá sa pripojiť k gateway.", + "mqtt_component": "MQTT komponent je nedostupný.", + "unable_to_calibrate": "V procese kalibrácie sa vyskytol problém a nebolo možné ho úspešne dokončiť. Uistite sa, že váš vykurovací systém funguje správne a že všetky požadované senzory sú pripojené a fungujú správne.\n\nAk budete mať aj naďalej problémy s kalibráciou, zvážte kontaktovanie nás so žiadosťou o ďalšiu pomoc. Ospravedlňujeme sa za spôsobené nepríjemnosti." + }, + "progress": { + "calibration": "Kalibrácia a nájdenie hodnoty ochrany proti prekročeniu...\n\nPočkajte, prosím, kým optimalizujeme váš vykurovací systém. Tento proces môže trvať približne 20 minút." + }, + "step": { + "areas": { + "data": { + "main_climates": "Primárne", + "secondary_climates": "Izby" + }, + "description": "Nastavenia súvisiace s klímou, viacerými miestnosťami a reguláciou teploty. Primárna klíma je v rovnakej miestnosti ako vnútorný snímač a miestnosti majú svoje vlastné cieľové teploty oddelené od systému.", + "title": "Oblasti" + }, + "automatic_gains": { + "data": { + "automatic_gains": "Automatické zisky (odporúčané)" + }, + "description": "Táto funkcia dynamicky upravuje parametre regulácie vášho vykurovacieho systému, čím optimalizuje reguláciu teploty pre lepší komfort a energetickú účinnosť. Povolenie tejto možnosti umožňuje SAT nepretržite prispôsobovať a dolaďovať nastavenia vykurovania na základe podmienok prostredia. Pomáha to udržiavať stabilné a pohodlné prostredie bez manuálneho zásahu.\n\nPoznámka: Ak sa rozhodnete nepovoliť automatické zosilnenie, budete musieť manuálne zadať hodnoty PID pre presnú reguláciu teploty. Uistite sa, že máte presné hodnoty PID pre váš konkrétny vykurovací systém, aby ste dosiahli optimálny výkon.", + "title": "Automatické zisky" + }, + "calibrate_system": { + "description": "Optimalizujte svoj vykurovací systém automatickým určením optimálnych hodnôt PID pre vaše nastavenie. Pri výbere automatického zisku majte na pamäti, že systém prejde procesom kalibrácie, ktorý môže trvať približne 20 minút.\n\nAutomatické zisky sa odporúčajú pre väčšinu používateľov, pretože zjednodušujú proces nastavenia a zaisťujú optimálny výkon. Ak však poznáte reguláciu PID a dávate prednosť manuálnemu nastaveniu hodnôt, môžete automatické zosilnenie preskočiť.\n\nUpozorňujeme, že ak sa rozhodnete preskočiť automatické zosilnenie, musíte dobre porozumieť regulácii PID a môže vyžadovať ďalšie manuálne úpravy na dosiahnutie optimálneho výkonu.", + "menu_options": { + "calibrate": "Kalibrujte a určte hodnotu ochrany proti prekročeniu (približne 20 min).", + "overshoot_protection": "Manuálne zadajte hodnotu ochrany proti prekročeniu.", + "pid_controller": "Ručne zadajte hodnoty PID (neodporúča sa)." + }, + "title": "Kalibrovať systém" + }, + "calibrated": { + "description": "Proces kalibrácie bol úspešne dokončený.\n\nBlahoželáme! Váš Smart Autotune Thermostat (SAT) bol kalibrovaný tak, aby optimalizoval vykurovací výkon vášho systému. Počas procesu kalibrácie SAT starostlivo analyzoval vykurovacie charakteristiky a určil vhodnú hodnotu ochrany proti prekročeniu, aby sa zabezpečila presná kontrola teploty.\n\nHodnota ochrany proti prekročeniu: {minimum_setpoint} °C\n\nTáto hodnota predstavuje maximálnu hodnotu prekročenia povolenú počas proces zahrievania. SAT bude aktívne monitorovať a upravovať vykurovanie, aby sa zabránilo nadmernému pretáčaniu, a udrží tak komfortné a efektívne vykurovanie vo vašej domácnosti.\n\nUpozorňujeme, že hodnota ochrany proti prekročeniu sa môže líšiť v závislosti od konkrétnych charakteristík vášho vykurovacieho systému a faktorov prostredia. Bol jemne vyladený tak, aby poskytoval optimálny výkon na základe výsledkov kalibrácie.", + "menu_options": { + "calibrate": "Zopakujte kalibráciu", + "finish": "Pokračujte v aktuálnej kalibrácii" + }, + "title": "Kalibrácia dokončená" + }, + "heating_system": { + "data": { + "heating_system": "Systém" + }, + "description": "Výber správneho typu vykurovacieho systému je pre SAT dôležitý na presné riadenie teploty a optimalizáciu výkonu. Vyberte si možnosť, ktorá zodpovedá vášmu nastaveniu, aby ste zabezpečili správnu reguláciu teploty v celom dome.", + "title": "Vykurovací systém" + }, + "mosquitto": { + "data": { + "device": "Zariadenia", + "mqtt_topic": "Hlavná téma", + "name": "Meno" + }, + "description": "Ak chcete nastaviť bránu OpenTherm, uveďte nasledujúce podrobnosti. Do poľa Názov zadajte názov brány, ktorý vám pomôže identifikovať ju vo vašom systéme.\n\nZadajte entitu Climate, ktorá sa má použiť pre bránu OpenTherm. Túto entitu poskytuje brána OpenTherm a predstavuje váš vykurovací systém.\n\nOkrem toho zadajte hlavnú tému, ktorá sa použije na publikovanie a prihlásenie na odber správ MQTT súvisiacich s bránou OpenTherm.\n\nTieto nastavenia sú nevyhnutné na nadviazanie komunikácie a integráciu s vašou bránou OpenTherm cez MQTT. Umožňujú bezproblémovú výmenu dát a ovládanie vášho vykurovacieho systému. Uistite sa, že poskytnuté podrobnosti sú presné, aby sa zabezpečila správna funkčnosť.", + "title": "OpenTherm Gateway ( MQTT )" + }, + "overshoot_protection": { + "data": { + "minimum_setpoint": "Hodnota" + }, + "description": "Poskytnutím hodnoty ochrany proti prekročeniu SAT prispôsobí parametre ovládania tak, aby sa udržalo stabilné a pohodlné vykurovacie prostredie. Táto manuálna konfigurácia vám umožňuje doladiť systém na základe vášho špecifického nastavenia.\n\nPoznámka: Ak si nie ste istí hodnotou ochrany proti prekročeniu alebo ste nevykonali proces kalibrácie, odporúča sa zrušiť konfiguráciu a prejsť proces kalibrácie, aby SAT automaticky určil hodnotu pre optimálny výkon.", + "title": "Ochrana proti prestreleniu" + }, + "pid_controller": { + "data": { + "derivative": "Derivačná (kD)", + "integral": "Integračná (kI)", + "proportional": "Proporcionálna (kP)" + }, + "description": "Manuálne nakonfigurujte proporcionálne, integrálne a derivačné zisky, aby ste doladili svoj vykurovací systém. Túto možnosť použite, ak chcete mať plnú kontrolu nad parametrami PID regulátora. Upravte zisky na základe špecifických charakteristík a preferencií vykurovacieho systému.", + "title": "Nakonfigurujte PID regulátor manuálne." + }, + "sensors": { + "data": { + "humidity_sensor_entity_id": "Entita snímača vlhkosti", + "inside_sensor_entity_id": "Vnútorná entita senzora", + "outside_sensor_entity_id": "Entita vonkajšieho senzora" + }, + "description": "Vyberte senzory, ktoré sa budú používať na sledovanie teploty.", + "title": "Nakonfigurujte senzory" + }, + "serial": { + "data": { + "device": "URL", + "name": "Názov" + }, + "description": "Ak chcete vytvoriť spojenie s bránou OpenTherm pomocou zásuvky, uveďte nasledujúce podrobnosti. Do poľa Názov zadajte názov brány, ktorý vám pomôže identifikovať ju vo vašom systéme.\n\nV poli Zariadenie zadajte sieťovú adresu brány OpenTherm. Môže to byť vo formáte \"socket://otgw.local:25238\", kde \"otgw.local\" je názov hostiteľa alebo IP adresa brány a \"25238\" je číslo portu.\ n\nTieto nastavenia sú nevyhnutné pre nadviazanie komunikácie a integráciu s vašou bránou OpenTherm cez pripojenie soketu. Uistite sa, že poskytnuté podrobnosti sú presné, aby sa zabezpečila správna funkčnosť.", + "title": "OpenTherm Gateway ( SERIAL )" + }, + "simulator": { + "data": { + "maximum_setpoint": "Maximálna požadovaná hodnota", + "minimum_setpoint": "Minimálna požadovaná hodnota", + "name": "Name", + "simulated_cooling": "Simulované chladenie", + "simulated_heating": "Simulovaný ohrev", + "simulated_warming_up": "Simulované zahrievanie" + }, + "description": "Táto brána vám umožňuje simulovať kotol na testovacie a demonštračné účely. Na konfiguráciu simulátora uveďte nasledujúce informácie.\n\nPoznámka: Brána simulátora je určená len na testovacie a demonštračné účely a nemala by sa používať v produkčnom prostredí.", + "title": "Simulovaný gateway (POKROČILÉ)" + }, + "switch": { + "data": { + "device": "Entita", + "minimum_setpoint": "Nastavenie teploty", + "name": "Názov" + }, + "description": "Ak chcete nastaviť prepínač, vyplňte nasledujúce údaje. Do poľa Názov zadajte názov prepínača, ktorý vám pomôže identifikovať ho vo vašom systéme. Z poskytnutých možností vyberte vhodnú entitu pre váš prepínač.\n\nV poli Nastavenie teploty zadajte požadovanú cieľovú teplotu pre váš vykurovací systém. Ak používate teplovodný bojler, vyplňte nastavenie teploty kotla príslušnou hodnotou. Pre elektrické vykurovacie systémy zadajte hodnotu 100.\n\nTieto nastavenia sú nevyhnutné pre presnú reguláciu teploty a zabezpečenie optimálneho výkonu vášho vykurovacieho systému. Poskytnutie správneho nastavenia teploty umožňuje presnú reguláciu a pomáha dosiahnuť pohodlné a energeticky efektívne prostredie vo vašej domácnosti.", + "title": "PID termostat s PWM (ON/OFF)" + }, + "user": { + "description": "SAT je inteligentný termostat, ktorý je schopný samočinného ladenia na optimalizáciu regulácie teploty. Vyberte vhodný režim, ktorý zodpovedá vášmu vykurovaciemu systému.", + "menu_options": { + "mosquitto": "OpenTherm Gateway ( MQTT )", + "serial": "OpenTherm Gateway ( SERIAL )", + "esphome": "OpenTherm Gateway ( ESPHOME )", + "simulator": "Simulated Gateway ( POKROČILÉ )", + "switch": "PID Thermostat with PWM ( ON/OFF )" + }, + "title": "Smart Autotune Thermostat (SAT)" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "climate_valve_offset": "Offset klimatizačného ventilu", + "dynamic_minimum_setpoint": "Dynamická minimálna nastavená hodnota (experimentálna)", + "force_pulse_width_modulation": "Vynútená modulácia šírky impulzu", + "maximum_consumption": "Maximálna spotreba", + "maximum_relative_modulation": "Maximálna relatívna modulácia", + "minimum_consumption": "Minimálna spotreba", + "sample_time": "Čas vzorky", + "simulation": "Simulácia", + "target_temperature_step": "Krok cieľovej teploty", + "thermal_comfort": "Tepelný komfort" + }, + "data_description": { + "climate_valve_offset": "Offset na nastavenie stupňa otvorenia klimatizačného ventilu.", + "dynamic_minimum_setpoint": "Aktivuje dynamické nastavenie požadovanej hodnoty na základe teploty spiatočky kotla, čo tiež pomáha identifikovať, či sú nejaké ventily zatvorené.", + "maximum_consumption": "Maximálna spotreba plynu, keď je kotol aktívny.", + "maximum_relative_modulation": "Predstavuje najvyššiu úroveň modulácie pre efektívny vykurovací systém.", + "minimum_consumption": "Minimálna spotreba plynu, keď je kotol aktívny.", + "sample_time": "Minimálny časový interval medzi aktualizáciami PID regulátora.", + "target_temperature_step": "Upravte krok cieľovej teploty pre jemné doladenie úrovní komfortu.", + "thermal_comfort": "Povoľte používanie indexu Simmer na nastavenie tepelného komfortu." + }, + "title": "Rozšírené" + }, + "general": { + "data": { + "automatic_gains_value": "Hodnota automatického zosilnenia", + "derivative": "Derivačná (kD)", + "derivative_time_weight": "Časová váha derivácie", + "duty_cycle": "Maximálny pracovný cyklus pre moduláciu šírky impulzu", + "heating_curve_coefficient": "Koeficient vykurovacej krivky", + "heating_curve_version": "Verzia vykurovacej krivky", + "integral": "Integračná (kI)", + "maximum_setpoint": "Maximálna požadovaná hodnota", + "minimum_setpoint_adjustment_factor": "Faktor nastavenia pre teplotu spiatočky", + "pid_controller_version": "Verzia PID regulátora", + "proportional": "Proporcionálna (kP)", + "sync_with_thermostat": "Synchronizujte požadovanú hodnotu s termostatom", + "window_sensors": "Kontaktné senzory", + "heating_mode": "Režim vykurovania" + }, + "data_description": { + "automatic_gains_value": "Hodnota používaná pre automatické zosilnenie v PID regulátore.", + "derivative": "Odvodený člen (kD) v PID regulátore, zodpovedný za zmiernenie prekročenia.", + "derivative_time_weight": "Parameter na úpravu vplyvu derivačného členu v priebehu času, obzvlášť užitočný na zníženie podkmitu počas zahrievacej fázy, keď je koeficient vykurovacej krivky správne nastavený.", + "duty_cycle": "Maximálny pracovný cyklus pre moduláciu šírky impulzu (PWM), ktorá riadi cykly zapnutia a vypnutia kotla.", + "heating_curve_coefficient": "Koeficient použitý na úpravu vykurovacej krivky.", + "integral": "Integrálny člen (kI) v regulátore PID, zodpovedný za zníženie chyby v ustálenom stave.", + "maximum_setpoint": "Optimálna teplota pre efektívnu prevádzku kotla.", + "minimum_setpoint_adjustment_factor": "Tento faktor upravuje žiadanú hodnotu vykurovania na základe teploty spiatočky kotla, čím ovplyvňuje odozvu a účinnosť vykurovania. Vyššia hodnota zvyšuje citlivosť na zmeny teploty, čím sa zlepšuje kontrola nad komfortom a spotrebou energie. Odporúčaný počiatočný rozsah je 0,1 až 0,5. Upravte ho tak, aby vyhovoval vášmu systému a preferenciám pohodlia.", + "proportional": "Proporcionálny člen (kP) v PID regulátore, zodpovedný za okamžitú reakciu na chyby.", + "sync_with_thermostat": "Synchronizujte požadovanú hodnotu s termostatom, aby ste zabezpečili koordinovanú reguláciu teploty.", + "window_sensors": "Kontaktné senzory, ktoré spúšťajú reakciu systému, keď je okno alebo dvere otvorené po určitú dobu." + }, + "description": "Všeobecné nastavenia a konfigurácie.", + "title": "Všeobecné" + }, + "init": { + "menu_options": { + "advanced": "Rozšírené možnosti", + "general": "VŠeobecné", + "presets": "Predvoľby", + "system_configuration": "Konfigurácia systému" + } + }, + "presets": { + "data": { + "activity_temperature": "Teplota aktivity", + "away_temperature": "Teplota pri neprítomnosti", + "comfort_temperature": "Komfortná teplota", + "home_temperature": "Domáca teplota", + "sleep_temperature": "Teplota spánku", + "sync_climates_with_preset": "Synchronizujte klímu s predvoľbou (spánok / preč / aktivita)" + }, + "description": "Preddefinované nastavenia teploty pre rôzne scenáre alebo činnosti.", + "title": "Predvoľby" + }, + "system_configuration": { + "data": { + "automatic_duty_cycle": "Automatický pracovný cyklus", + "overshoot_protection": "Ochrana proti prekročeniu (s PWM)", + "sensor_max_value_age": "Vek maximálnej hodnoty snímača teploty", + "window_minimum_open_time": "Minimálny čas na otvorenie okna" + }, + "data_description": { + "automatic_duty_cycle": "Povoliť alebo zakázať automatický pracovný cyklus pre moduláciu šírky impulzu (PWM).", + "overshoot_protection": "Aktivujte ochranu proti prekročeniu pomocou modulácie šírky impulzov (PWM), aby ste zabránili prekročeniu teploty kotla.", + "sensor_max_value_age": "Maximálny vek hodnoty teplotného snímača pred tým, než sa považuje za prerušenie.", + "window_minimum_open_time": "Minimálny čas, počas ktorého musí byť okno otvorené, kým systém zareaguje." + }, + "description": "Na jemné doladenie a prispôsobenie.", + "title": "Konfigurácia systému" + } + } + } +} From d8442bbd3d8e0ccf9bbe08ad5ffadf0b54cdc966 Mon Sep 17 00:00:00 2001 From: misa1515 <61636045+misa1515@users.noreply.github.com> Date: Wed, 16 Oct 2024 08:43:15 +0200 Subject: [PATCH 076/213] Update sk.json --- custom_components/sat/translations/sk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/translations/sk.json b/custom_components/sat/translations/sk.json index 2b4a6426..ec8d4f14 100644 --- a/custom_components/sat/translations/sk.json +++ b/custom_components/sat/translations/sk.json @@ -91,7 +91,7 @@ "device": "URL", "name": "Názov" }, - "description": "Ak chcete vytvoriť spojenie s bránou OpenTherm pomocou zásuvky, uveďte nasledujúce podrobnosti. Do poľa Názov zadajte názov brány, ktorý vám pomôže identifikovať ju vo vašom systéme.\n\nV poli Zariadenie zadajte sieťovú adresu brány OpenTherm. Môže to byť vo formáte \"socket://otgw.local:25238\", kde \"otgw.local\" je názov hostiteľa alebo IP adresa brány a \"25238\" je číslo portu.\ n\nTieto nastavenia sú nevyhnutné pre nadviazanie komunikácie a integráciu s vašou bránou OpenTherm cez pripojenie soketu. Uistite sa, že poskytnuté podrobnosti sú presné, aby sa zabezpečila správna funkčnosť.", + "description": "Ak chcete vytvoriť spojenie s bránou OpenTherm pomocou zásuvky, uveďte nasledujúce podrobnosti. Do poľa Názov zadajte názov brány, ktorý vám pomôže identifikovať ju vo vašom systéme.\n\nV poli Zariadenie zadajte sieťovú adresu brány OpenTherm. Môže to byť vo formáte \"socket://otgw.local:25238\", kde \"otgw.local\" je názov hostiteľa alebo IP adresa brány a \"25238\" je číslo portu.\n\nTieto nastavenia sú nevyhnutné pre nadviazanie komunikácie a integráciu s vašou bránou OpenTherm cez pripojenie soketu. Uistite sa, že poskytnuté podrobnosti sú presné, aby sa zabezpečila správna funkčnosť.", "title": "OpenTherm Gateway ( SERIAL )" }, "simulator": { From 6269053957a2928f9953e2d5638a13fcddbb5021 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 30 Oct 2024 17:30:52 +0100 Subject: [PATCH 077/213] Some sanity checks --- custom_components/sat/pwm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index e867a94b..7ada347e 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -95,7 +95,7 @@ def _calculate_duty_cycle(self, requested_setpoint: float) -> Optional[Tuple[int boiler_temperature = self._last_boiler_temperature or requested_setpoint base_offset = self._heating_curve.base_offset - if boiler_temperature < base_offset: + if boiler_temperature <= base_offset: boiler_temperature = base_offset + 1 self._last_duty_cycle_percentage = (requested_setpoint - base_offset) / (boiler_temperature - base_offset) From e319ac4193eaaffba5df95732c299e1192c80514 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 15 Nov 2024 20:34:24 +0100 Subject: [PATCH 078/213] Add support for toggling to keep the modes in sync --- custom_components/sat/climate.py | 10 ++++++++-- custom_components/sat/config_flow.py | 1 + custom_components/sat/const.py | 2 ++ custom_components/sat/translations/de.json | 1 + custom_components/sat/translations/en.json | 1 + custom_components/sat/translations/es.json | 1 + custom_components/sat/translations/fr.json | 1 + custom_components/sat/translations/it.json | 1 + custom_components/sat/translations/nl.json | 1 + custom_components/sat/translations/pt.json | 1 + 10 files changed, 18 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index e2172f4e..61f02016 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -167,6 +167,7 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn self._climate_valve_offset = float(config_options.get(CONF_CLIMATE_VALVE_OFFSET)) self._target_temperature_step = float(config_options.get(CONF_TARGET_TEMPERATURE_STEP)) self._dynamic_minimum_setpoint = bool(config_options.get(CONF_DYNAMIC_MINIMUM_SETPOINT)) + self._sync_climates_with_mode = bool(config_options.get(CONF_SYNC_CLIMATES_WITH_MODE)) self._sync_climates_with_preset = bool(config_options.get(CONF_SYNC_CLIMATES_WITH_PRESET)) self._maximum_relative_modulation = int(config_options.get(CONF_MAXIMUM_RELATIVE_MODULATION)) self._force_pulse_width_modulation = bool(config_options.get(CONF_FORCE_PULSE_WIDTH_MODULATION)) @@ -936,8 +937,13 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: # Reset the PID controller await self._async_control_pid(True) - # Set the hvac mode for all climate devices - for entity_id in (self._climates + self._main_climates): + # Collect which climates to control + climates = self._main_climates[:] + if self._sync_climates_with_mode: + climates += self._climates + + # Set the hvac mode for those climate devices + for entity_id in climates: data = {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: hvac_mode} await self.hass.services.async_call(CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, data, blocking=True) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index cf54b4a6..0af33c07 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -569,6 +569,7 @@ async def async_step_system_configuration(self, _user_input: dict[str, Any] | No step_id="system_configuration", data_schema=vol.Schema({ vol.Required(CONF_AUTOMATIC_DUTY_CYCLE, default=options[CONF_AUTOMATIC_DUTY_CYCLE]): bool, + vol.Required(CONF_SYNC_CLIMATES_WITH_MODE, default=options[CONF_SYNC_CLIMATES_WITH_MODE]): bool, vol.Required(CONF_SENSOR_MAX_VALUE_AGE, default=options[CONF_SENSOR_MAX_VALUE_AGE]): selector.TimeSelector(), vol.Required(CONF_WINDOW_MINIMUM_OPEN_TIME, default=options[CONF_WINDOW_MINIMUM_OPEN_TIME]): selector.TimeSelector(), }) diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index 62dbe44e..f4648daf 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -53,6 +53,7 @@ CONF_CLIMATE_VALVE_OFFSET = "climate_valve_offset" CONF_SENSOR_MAX_VALUE_AGE = "sensor_max_value_age" CONF_OVERSHOOT_PROTECTION = "overshoot_protection" +CONF_SYNC_CLIMATES_WITH_MODE = "sync_climates_with_mode" CONF_SYNC_CLIMATES_WITH_PRESET = "sync_climates_with_preset" CONF_FORCE_PULSE_WIDTH_MODULATION = "force_pulse_width_modulation" CONF_TARGET_TEMPERATURE_STEP = "target_temperature_step" @@ -107,6 +108,7 @@ CONF_THERMAL_COMFORT: False, CONF_HUMIDITY_SENSOR_ENTITY_ID: None, CONF_SYNC_WITH_THERMOSTAT: False, + CONF_SYNC_CLIMATES_WITH_MODE: True, CONF_SYNC_CLIMATES_WITH_PRESET: False, CONF_SIMULATED_HEATING: 20, diff --git a/custom_components/sat/translations/de.json b/custom_components/sat/translations/de.json index 7f06715c..ca35a859 100644 --- a/custom_components/sat/translations/de.json +++ b/custom_components/sat/translations/de.json @@ -211,6 +211,7 @@ "data": { "automatic_duty_cycle": "Automatischer Tastgrad", "overshoot_protection": "Überschwingungsschutz (mit PWM)", + "sync_climates_with_mode": "Klimazonen mit Modus synchronisieren", "sensor_max_value_age": "Maximales Alter des Temperatursensorwerts", "window_minimum_open_time": "Mindestzeit für geöffnetes Fenster" }, diff --git a/custom_components/sat/translations/en.json b/custom_components/sat/translations/en.json index 62c0236e..6538b6fb 100644 --- a/custom_components/sat/translations/en.json +++ b/custom_components/sat/translations/en.json @@ -211,6 +211,7 @@ "system_configuration": { "data": { "automatic_duty_cycle": "Automatic duty cycle", + "sync_climates_with_mode": "Synchronize climates with mode", "overshoot_protection": "Overshoot Protection (with PWM)", "sensor_max_value_age": "Temperature Sensor maximum value age", "window_minimum_open_time": "Minimum time for window to be open" diff --git a/custom_components/sat/translations/es.json b/custom_components/sat/translations/es.json index dc3d84b5..aac024a3 100644 --- a/custom_components/sat/translations/es.json +++ b/custom_components/sat/translations/es.json @@ -210,6 +210,7 @@ "system_configuration": { "data": { "automatic_duty_cycle": "Ciclo de trabajo automático", + "sync_climates_with_mode": "Sincronizar climas con el modo", "overshoot_protection": "Protección contra Sobrepasos (con PWM)", "sensor_max_value_age": "Edad máxima del valor del sensor de temperatura", "window_minimum_open_time": "Tiempo mínimo de apertura de ventana" diff --git a/custom_components/sat/translations/fr.json b/custom_components/sat/translations/fr.json index a053c16e..1918e566 100644 --- a/custom_components/sat/translations/fr.json +++ b/custom_components/sat/translations/fr.json @@ -210,6 +210,7 @@ "system_configuration": { "data": { "automatic_duty_cycle": "Cycle de fonctionnement automatique", + "sync_climates_with_mode": "Synchroniser les climats avec le mode", "overshoot_protection": "Protection contre le dépassement (avec PWM)", "sensor_max_value_age": "Âge maximal de la valeur du capteur de température", "window_minimum_open_time": "Temps minimum d'ouverture de la fenêtre" diff --git a/custom_components/sat/translations/it.json b/custom_components/sat/translations/it.json index 80d12f06..b9b15f10 100644 --- a/custom_components/sat/translations/it.json +++ b/custom_components/sat/translations/it.json @@ -210,6 +210,7 @@ "system_configuration": { "data": { "automatic_duty_cycle": "Ciclo di lavoro automatico", + "sync_climates_with_mode": "Sincronizza climi con la modalità", "overshoot_protection": "Protezione dal Superamento (con PWM)", "sensor_max_value_age": "Età massima del valore del sensore di temperatura", "window_minimum_open_time": "Tempo minimo di apertura della finestra" diff --git a/custom_components/sat/translations/nl.json b/custom_components/sat/translations/nl.json index a21663b2..aeaa72c9 100644 --- a/custom_components/sat/translations/nl.json +++ b/custom_components/sat/translations/nl.json @@ -211,6 +211,7 @@ "data": { "automatic_duty_cycle": "Automatische inschakelduur", "overshoot_protection": "Overshoot Bescherming (met PWM)", + "sync_climates_with_mode": "Synchroniseer klimaten met modus", "sensor_max_value_age": "Maximale leeftijd van de waarden van de temperatuursensor", "window_minimum_open_time": "Minimale open tijd van het raam" }, diff --git a/custom_components/sat/translations/pt.json b/custom_components/sat/translations/pt.json index b776ae12..cbe42414 100644 --- a/custom_components/sat/translations/pt.json +++ b/custom_components/sat/translations/pt.json @@ -211,6 +211,7 @@ "system_configuration": { "data": { "automatic_duty_cycle": "Ciclo de trabalho automático", + "sync_climates_with_mode": "Sincronizar climas com o modo", "overshoot_protection": "Proteção contra Overshoot (com PWM)", "sensor_max_value_age": "Idade máxima do valor do sensor de temperatura", "window_minimum_open_time": "Tempo mínimo para a janela estar aberta" From 6bd179f443d14b997b84d86701048f8b4b3e6478 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 15 Nov 2024 20:39:53 +0100 Subject: [PATCH 079/213] Fixed version --- custom_components/sat/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/manifest.json b/custom_components/sat/manifest.json index 61c54398..6b0939d1 100644 --- a/custom_components/sat/manifest.json +++ b/custom_components/sat/manifest.json @@ -24,5 +24,5 @@ "requirements": [ "pyotgw==2.1.3" ], - "version": "3.0.1" + "version": "3.1.0" } \ No newline at end of file From ae591bb20836d00a47629e413bd80e54e6bb309b Mon Sep 17 00:00:00 2001 From: Conrad Juhl Andersen Date: Thu, 21 Nov 2024 08:00:50 +0100 Subject: [PATCH 080/213] Updated home assistant entity names to offical esphome names --- custom_components/sat/esphome/__init__.py | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index bc3d33a2..c707c12c 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -17,26 +17,26 @@ from ..coordinator import DeviceState, SatDataUpdateCoordinator, SatEntityCoordinator # Sensors -DATA_FLAME_ACTIVE = "flame_active" -DATA_REL_MOD_LEVEL = "modulation" -DATA_SLAVE_MEMBERID = "boiler_member_id" -DATA_BOILER_TEMPERATURE = "boiler_temperature" -DATA_RETURN_TEMPERATURE = "return_temperature" +DATA_FLAME_ACTIVE = "flame_on" +DATA_REL_MOD_LEVEL = "rel_mod_level" +DATA_SLAVE_MEMBERID = "device_id" +DATA_BOILER_TEMPERATURE = "t_boiler" +DATA_RETURN_TEMPERATURE = "t_ret" DATA_BOILER_CAPACITY = "max_capacity" DATA_REL_MIN_MOD_LEVEL = "min_mod_level" -DATA_DHW_SETPOINT_MINIMUM = "dhw_min_temperature" -DATA_DHW_SETPOINT_MAXIMUM = "dhw_max_temperature" +DATA_DHW_SETPOINT_MINIMUM = "t_dhw_set_lb" +DATA_DHW_SETPOINT_MAXIMUM = "t_dhw_set_ub" # Switch -DATA_DHW_ENABLE = "dhw_enabled" -DATA_CENTRAL_HEATING = "ch_enabled" +DATA_DHW_ENABLE = "dhw_enable" +DATA_CENTRAL_HEATING = "ch_enable" # Number -DATA_DHW_SETPOINT = "dhw_setpoint_temperature" -DATA_CONTROL_SETPOINT = "ch_setpoint_temperature" -DATA_MAX_CH_SETPOINT = "max_ch_setpoint_temperature" -DATA_MAX_REL_MOD_LEVEL_SETTING = "max_modulation_level" +DATA_DHW_SETPOINT = "t_dhw_set" +DATA_CONTROL_SETPOINT = "t_set" +DATA_MAX_CH_SETPOINT = "max_t_set" +DATA_MAX_REL_MOD_LEVEL_SETTING = "max_rel_mod_level" if TYPE_CHECKING: from ..climate import SatClimate From ea4d6b142f868f63f36cd4bf745bb6fe0eae378f Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 23 Nov 2024 15:49:19 +0100 Subject: [PATCH 081/213] Make the PWM also listen to the flame and hot water --- custom_components/sat/boiler_state.py | 52 +++++++++++++++++++++++++++ custom_components/sat/pwm.py | 27 +++++++------- 2 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 custom_components/sat/boiler_state.py diff --git a/custom_components/sat/boiler_state.py b/custom_components/sat/boiler_state.py new file mode 100644 index 00000000..2c901ff7 --- /dev/null +++ b/custom_components/sat/boiler_state.py @@ -0,0 +1,52 @@ +class BoilerState: + """ + Represents the operational state of a boiler, including activity, flame status, + hot water usage, and current temperature. + """ + def __init__( + self, + device_active: bool, + flame_active: bool, + hot_water_active: bool, + temperature: float + ): + """ + Initialize with the boiler's state parameters. + + :param device_active: Whether the boiler is currently operational. + :param flame_active: Whether the boiler's flame is ignited. + :param hot_water_active: Whether the boiler is heating water. + :param temperature: The current boiler temperature in degrees Celsius. + """ + self._device_active = device_active + self._flame_active = flame_active + self._hot_water_active = hot_water_active + self._temperature = temperature + + @property + def device_active(self) -> bool: + """ + Indicates whether the boiler is running. + """ + return self._device_active + + @property + def flame_active(self) -> bool: + """ + Indicates whether the flame is ignited. + """ + return self._flame_active + + @property + def hot_water_active(self) -> bool: + """ + Indicates whether the boiler is heating water. + """ + return self._hot_water_active + + @property + def temperature(self) -> float: + """ + The boiler's current temperature. + """ + return self._temperature diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index 7ada347e..73f684b7 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -3,6 +3,7 @@ from time import monotonic from typing import Optional, Tuple +from .boiler_state import BoilerState from .const import HEATER_STARTUP_TIMEFRAME from .heating_curve import HeatingCurve @@ -45,7 +46,7 @@ def reset(self) -> None: self._state = PWMState.IDLE self._last_update = monotonic() - async def update(self, requested_setpoint: float, boiler_temperature: float) -> None: + async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: """Update the PWM state based on the output of a PID controller.""" if not self._heating_curve.value: self._state = PWMState.IDLE @@ -56,29 +57,29 @@ async def update(self, requested_setpoint: float, boiler_temperature: float) -> if requested_setpoint is None: self._state = PWMState.IDLE self._last_update = monotonic() - self._last_boiler_temperature = boiler_temperature + self._last_boiler_temperature = boiler.temperature _LOGGER.debug("Turned off PWM due since we do not have a valid requested setpoint.") return - if boiler_temperature is not None and self._last_boiler_temperature is None: - self._last_boiler_temperature = boiler_temperature + if boiler.temperature is not None and self._last_boiler_temperature is None: + self._last_boiler_temperature = boiler.temperature elapsed = monotonic() - self._last_update - self._duty_cycle = self._calculate_duty_cycle(requested_setpoint) + self._duty_cycle = self._calculate_duty_cycle(requested_setpoint, boiler) _LOGGER.debug("Calculated duty cycle %.0f seconds ON", self._duty_cycle[0]) _LOGGER.debug("Calculated duty cycle %.0f seconds OFF", self._duty_cycle[1]) - if self._state == PWMState.ON and boiler_temperature is not None: + if self._state == PWMState.ON and boiler.temperature is not None: if elapsed <= HEATER_STARTUP_TIMEFRAME: - self._last_boiler_temperature = self._alpha * boiler_temperature + (1 - self._alpha) * self._last_boiler_temperature + self._last_boiler_temperature = self._alpha * boiler.temperature + (1 - self._alpha) * self._last_boiler_temperature else: - self._last_boiler_temperature = boiler_temperature + self._last_boiler_temperature = boiler.temperature if self._state != PWMState.ON and self._duty_cycle[0] >= HEATER_STARTUP_TIMEFRAME and (elapsed >= self._duty_cycle[1] or self._state == PWMState.IDLE): self._state = PWMState.ON self._last_update = monotonic() - self._last_boiler_temperature = boiler_temperature or 0 + self._last_boiler_temperature = boiler.temperature or 0 _LOGGER.debug("Starting duty cycle.") return @@ -90,7 +91,7 @@ async def update(self, requested_setpoint: float, boiler_temperature: float) -> _LOGGER.debug("Cycle time elapsed %.0f seconds in %s", elapsed, self._state) - def _calculate_duty_cycle(self, requested_setpoint: float) -> Optional[Tuple[int, int]]: + def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) -> Optional[Tuple[int, int]]: """Calculates the duty cycle in seconds based on the output of a PID controller and a heating curve value.""" boiler_temperature = self._last_boiler_temperature or requested_setpoint base_offset = self._heating_curve.base_offset @@ -109,8 +110,8 @@ def _calculate_duty_cycle(self, requested_setpoint: float) -> Optional[Tuple[int if not self._automatic_duty_cycle: return int(self._last_duty_cycle_percentage * self._max_cycle_time), int((1 - self._last_duty_cycle_percentage) * self._max_cycle_time) - if self._last_duty_cycle_percentage < MIN_DUTY_CYCLE_PERCENTAGE: - return 0, 1800 + if self._last_duty_cycle_percentage < MIN_DUTY_CYCLE_PERCENTAGE and boiler.flame_active and not boiler.hot_water_active: + return 180, 1620 if self._last_duty_cycle_percentage <= DUTY_CYCLE_20_PERCENT: on_time = ON_TIME_20_PERCENT @@ -133,6 +134,8 @@ def _calculate_duty_cycle(self, requested_setpoint: float) -> Optional[Tuple[int if self._last_duty_cycle_percentage > MAX_DUTY_CYCLE_PERCENTAGE: return 1800, 0 + return 0, 1800 + @property def state(self) -> PWMState: """Returns the current state of the PWM control.""" From 205d0213277d82a4f727bc2a2a7499e92391fc21 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 23 Nov 2024 15:55:43 +0100 Subject: [PATCH 082/213] Improve default values --- custom_components/sat/const.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index f4648daf..2f5b0d62 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -95,8 +95,8 @@ CONF_AUTOMATIC_GAINS: True, CONF_AUTOMATIC_DUTY_CYCLE: True, - CONF_AUTOMATIC_GAINS_VALUE: 5.0, - CONF_DERIVATIVE_TIME_WEIGHT: 6.0, + CONF_AUTOMATIC_GAINS_VALUE: 2.0, + CONF_DERIVATIVE_TIME_WEIGHT: 2.5, CONF_OVERSHOOT_PROTECTION: False, CONF_DYNAMIC_MINIMUM_SETPOINT: False, CONF_MINIMUM_SETPOINT_ADJUSTMENT_FACTOR: 0.2, @@ -137,7 +137,7 @@ CONF_COMFORT_TEMPERATURE: 20, CONF_HEATING_CURVE_VERSION: 3, - CONF_HEATING_CURVE_COEFFICIENT: 1.0, + CONF_HEATING_CURVE_COEFFICIENT: 2.0, CONF_HEATING_MODE: HEATING_MODE_COMFORT, CONF_HEATING_SYSTEM: HEATING_SYSTEM_RADIATORS, From 621e72a1a297b6a2104362180eccfbd0e6a496eb Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 23 Nov 2024 15:57:29 +0100 Subject: [PATCH 083/213] Lower the Ki denominator --- custom_components/sat/pid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/pid.py b/custom_components/sat/pid.py index 30f49161..b13c66fe 100644 --- a/custom_components/sat/pid.py +++ b/custom_components/sat/pid.py @@ -299,7 +299,7 @@ def ki(self) -> float | None: return round(self._last_heating_curve_value / 73900, 6) if self._version == 2: - return round(self._automatic_gains_value * (self._last_heating_curve_value / 36000), 6) + return round(self._automatic_gains_value * (self._last_heating_curve_value / 7200), 6) raise Exception("Invalid version") From 3aa5749ee84a5ff1a13b838e78c66dc8246ab4d5 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 23 Nov 2024 16:06:46 +0100 Subject: [PATCH 084/213] Make sure we pass the state along --- custom_components/sat/climate.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 61f02016..dba83560 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -36,6 +36,7 @@ from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity +from .boiler_state import BoilerState from .const import * from .coordinator import SatDataUpdateCoordinator, DeviceState from .entity import SatEntity @@ -873,7 +874,14 @@ async def async_control_heating_loop(self, _time=None) -> None: # Pulse Width Modulation if self.pulse_width_modulation_enabled: - await self.pwm.update(self._calculated_setpoint, self._coordinator.boiler_temperature) + boiler_state = BoilerState( + flame_active=self._coordinator.flame_active, + device_active=self._coordinator.device_active, + hot_water_active=self._coordinator.hot_water_active, + temperature=self._coordinator.filtered_boiler_temperature + ) + + await self.pwm.update(self._calculated_setpoint, boiler_state) else: self.pwm.reset() From bf7f2439d48d9ceeff99e8e64637af7fe7216158 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 23 Nov 2024 16:10:27 +0100 Subject: [PATCH 085/213] Update tests --- tests/test_climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_climate.py b/tests/test_climate.py index 6efc2080..a0616f0a 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -51,7 +51,7 @@ async def test_scenario_1(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.heating_curve.value == 32.6 assert climate.pulse_width_modulation_enabled - assert climate.pwm.last_duty_cycle_percentage == 36.24 + assert climate.pwm.last_duty_cycle_percentage == 100 assert climate.pwm.duty_cycle == (326, 573) @@ -92,7 +92,7 @@ async def test_scenario_2(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.setpoint == 58 assert climate.heating_curve.value == 30.1 - assert climate.requested_setpoint == 30.6 + assert climate.requested_setpoint == 30.3 assert climate.pulse_width_modulation_enabled assert climate.pwm.last_duty_cycle_percentage == 11.04 @@ -136,7 +136,7 @@ async def test_scenario_3(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.setpoint == 41 assert climate.heating_curve.value == 32.1 - assert climate.requested_setpoint == 37.4 + assert climate.requested_setpoint == 34.2 assert climate.pulse_width_modulation_enabled assert climate.pwm.last_duty_cycle_percentage == 73.91 From 52a64ac2eabff856a20858e78fa954c7ce841122 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 23 Nov 2024 16:13:05 +0100 Subject: [PATCH 086/213] Update tests --- tests/test_climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_climate.py b/tests/test_climate.py index a0616f0a..6b7d9515 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -95,7 +95,7 @@ async def test_scenario_2(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.requested_setpoint == 30.3 assert climate.pulse_width_modulation_enabled - assert climate.pwm.last_duty_cycle_percentage == 11.04 + assert climate.pwm.last_duty_cycle_percentage == 100 assert climate.pwm.duty_cycle == (180, 1450) @@ -139,5 +139,5 @@ async def test_scenario_3(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.requested_setpoint == 34.2 assert climate.pulse_width_modulation_enabled - assert climate.pwm.last_duty_cycle_percentage == 73.91 + assert climate.pwm.last_duty_cycle_percentage == 100 assert climate.pwm.duty_cycle == (665, 234) From c4f820a55831dec73e7c78f6bab6d7ecaf47a185 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 23 Nov 2024 16:17:44 +0100 Subject: [PATCH 087/213] Update tests --- tests/test_climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_climate.py b/tests/test_climate.py index 6b7d9515..04c9f1fb 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -52,7 +52,7 @@ async def test_scenario_1(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.pulse_width_modulation_enabled assert climate.pwm.last_duty_cycle_percentage == 100 - assert climate.pwm.duty_cycle == (326, 573) + assert climate.pwm.duty_cycle == (1800, 0) @pytest.mark.parametrize(*[ @@ -96,7 +96,7 @@ async def test_scenario_2(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.pulse_width_modulation_enabled assert climate.pwm.last_duty_cycle_percentage == 100 - assert climate.pwm.duty_cycle == (180, 1450) + assert climate.pwm.duty_cycle == (1800, 0) @pytest.mark.parametrize(*[ @@ -140,4 +140,4 @@ async def test_scenario_3(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.pulse_width_modulation_enabled assert climate.pwm.last_duty_cycle_percentage == 100 - assert climate.pwm.duty_cycle == (665, 234) + assert climate.pwm.duty_cycle == (1800, 0) From 96867d39ec7a77ef58d075256459f02a890b7e44 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 23 Nov 2024 16:18:13 +0100 Subject: [PATCH 088/213] Drop support for the classic curve --- custom_components/sat/__init__.py | 36 +++++++++++++------------- custom_components/sat/config_flow.py | 3 +-- custom_components/sat/heating_curve.py | 3 --- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index 3472e0d6..fd777345 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -88,33 +88,33 @@ async def async_migrate_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool new_options = {**_entry.options} if _entry.version < 2: - if not _entry.data.get(CONF_MINIMUM_SETPOINT): + if not _entry.data.get("minimum_setpoint"): # Legacy Store store = Store(_hass, 1, DOMAIN) - new_data[CONF_MINIMUM_SETPOINT] = MINIMUM_SETPOINT + new_data["minimum_setpoint"] = 10 if (data := await store.async_load()) and (overshoot_protection_value := data.get("overshoot_protection_value")): - new_data[CONF_MINIMUM_SETPOINT] = overshoot_protection_value + new_data["minimum_setpoint"] = overshoot_protection_value if _entry.options.get("heating_system") == "underfloor": - new_data[CONF_HEATING_SYSTEM] = HEATING_SYSTEM_UNDERFLOOR + new_data["heating_system"] = "underfloor" else: - new_data[CONF_HEATING_SYSTEM] = HEATING_SYSTEM_RADIATORS + new_data["heating_system"] = "radiators" - if not _entry.data.get(CONF_MAXIMUM_SETPOINT): - new_data[CONF_MAXIMUM_SETPOINT] = 55 + if not _entry.data.get("maximum_setpoint"): + new_data["maximum_setpoint"] = 55 if _entry.options.get("heating_system") == "underfloor": - new_data[CONF_MAXIMUM_SETPOINT] = 50 + new_data["maximum_setpoint"] = 50 if _entry.options.get("heating_system") == "radiator_low_temperatures": - new_data[CONF_MAXIMUM_SETPOINT] = 55 + new_data["maximum_setpoint"] = 55 if _entry.options.get("heating_system") == "radiator_medium_temperatures": - new_data[CONF_MAXIMUM_SETPOINT] = 65 + new_data["maximum_setpoint"] = 65 if _entry.options.get("heating_system") == "radiator_high_temperatures": - new_data[CONF_MAXIMUM_SETPOINT] = 75 + new_data["maximum_setpoint"] = 75 if _entry.version < 3: if main_climates := _entry.options.get("main_climates"): @@ -122,16 +122,16 @@ async def async_migrate_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool new_options.pop("main_climates") if secondary_climates := _entry.options.get("climates"): - new_data[CONF_SECONDARY_CLIMATES] = secondary_climates + new_data["secondary_climates"] = secondary_climates new_options.pop("climates") if sync_with_thermostat := _entry.options.get("sync_with_thermostat"): - new_data[CONF_SYNC_WITH_THERMOSTAT] = sync_with_thermostat + new_data["sync_with_thermostat"] = sync_with_thermostat new_options.pop("sync_with_thermostat") if _entry.version < 4: if _entry.data.get("window_sensor") is not None: - new_data[CONF_WINDOW_SENSORS] = [_entry.data.get("window_sensor")] + new_data["window_sensors"] = [_entry.data.get("window_sensor")] del new_options["window_sensor"] if _entry.version < 5: @@ -139,11 +139,11 @@ async def async_migrate_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool new_data[CONF_OVERSHOOT_PROTECTION] = _entry.options.get("overshoot_protection") del new_options["overshoot_protection"] - if _entry.version < 6: - new_options[CONF_HEATING_CURVE_VERSION] = 1 - if _entry.version < 7: - new_options[CONF_PID_CONTROLLER_VERSION] = 1 + new_options["pid_controller_version"] = 1 + + if _entry.version < 8 and _entry.options.get("heating_curve_version") < 2: + new_options["heating_curve_version"] = 3 _entry.version = SatFlowHandler.VERSION _hass.config_entries.async_update_entry(_entry, data=new_data, options=new_options) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 0af33c07..4ef54bfe 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -36,7 +36,7 @@ class SatFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for SAT.""" - VERSION = 7 + VERSION = 8 MINOR_VERSION = 0 calibration = None @@ -468,7 +468,6 @@ async def async_step_general(self, _user_input: dict[str, Any] | None = None): schema[vol.Required(CONF_HEATING_CURVE_VERSION, default=str(options[CONF_HEATING_CURVE_VERSION]))] = selector.SelectSelector( selector.SelectSelectorConfig(mode=SelectSelectorMode.DROPDOWN, options=[ - selector.SelectOptionDict(value="1", label="Classic Curve"), selector.SelectOptionDict(value="2", label="Quantum Curve"), selector.SelectOptionDict(value="3", label="Precision Curve"), ]) diff --git a/custom_components/sat/heating_curve.py b/custom_components/sat/heating_curve.py index ba5e5884..3197a95a 100644 --- a/custom_components/sat/heating_curve.py +++ b/custom_components/sat/heating_curve.py @@ -72,9 +72,6 @@ def restore_autotune(self, coefficient: float, derivative: float): def _get_heating_curve_value(self, target_temperature: float, outside_temperature: float) -> float: """Calculate the heating curve value based on the current outside temperature""" - if self._version == 1: - return target_temperature - (0.01 * outside_temperature ** 2) - (0.8 * outside_temperature) - if self._version == 2: return 2.72 * (target_temperature - 20) + 0.03 * (outside_temperature - 20) ** 2 - 1.2 * (outside_temperature - 20) From 504776a97b790dff5d32e4070e66a580debe3cb8 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 23 Nov 2024 16:24:01 +0100 Subject: [PATCH 089/213] Make sure the version has changed first --- custom_components/sat/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index fd777345..cb59199b 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -142,8 +142,9 @@ async def async_migrate_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool if _entry.version < 7: new_options["pid_controller_version"] = 1 - if _entry.version < 8 and _entry.options.get("heating_curve_version") < 2: - new_options["heating_curve_version"] = 3 + if _entry.version < 8: + if _entry.options.get("heating_curve_version") is not None and _entry.options.get("heating_curve_version") < 2: + new_options["heating_curve_version"] = 3 _entry.version = SatFlowHandler.VERSION _hass.config_entries.async_update_entry(_entry, data=new_data, options=new_options) From 0bece8ce9b87158955a713f8e7d85fafbafc6632 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 23 Nov 2024 16:26:27 +0100 Subject: [PATCH 090/213] Update tests (again) --- tests/test_climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_climate.py b/tests/test_climate.py index 04c9f1fb..8e9535b7 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -48,7 +48,7 @@ async def test_scenario_1(hass: HomeAssistant, entry: MockConfigEntry, climate: await climate.async_set_hvac_mode(HVACMode.HEAT) assert climate.setpoint == 57 - assert climate.heating_curve.value == 32.6 + assert climate.heating_curve.value == 32.2 assert climate.pulse_width_modulation_enabled assert climate.pwm.last_duty_cycle_percentage == 100 @@ -91,7 +91,7 @@ async def test_scenario_2(hass: HomeAssistant, entry: MockConfigEntry, climate: await climate.async_set_hvac_mode(HVACMode.HEAT) assert climate.setpoint == 58 - assert climate.heating_curve.value == 30.1 + assert climate.heating_curve.value == 27.8 assert climate.requested_setpoint == 30.3 assert climate.pulse_width_modulation_enabled @@ -135,7 +135,7 @@ async def test_scenario_3(hass: HomeAssistant, entry: MockConfigEntry, climate: await climate.async_set_hvac_mode(HVACMode.HEAT) assert climate.setpoint == 41 - assert climate.heating_curve.value == 32.1 + assert climate.heating_curve.value == 32.5 assert climate.requested_setpoint == 34.2 assert climate.pulse_width_modulation_enabled From 32b40043a0633e81b6700864e81eff856886736e Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 23 Nov 2024 16:28:12 +0100 Subject: [PATCH 091/213] Update tests (again) --- tests/test_climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_climate.py b/tests/test_climate.py index 8e9535b7..f21f2845 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -92,7 +92,7 @@ async def test_scenario_2(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.setpoint == 58 assert climate.heating_curve.value == 27.8 - assert climate.requested_setpoint == 30.3 + assert climate.requested_setpoint == 28.0 assert climate.pulse_width_modulation_enabled assert climate.pwm.last_duty_cycle_percentage == 100 @@ -136,7 +136,7 @@ async def test_scenario_3(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.setpoint == 41 assert climate.heating_curve.value == 32.5 - assert climate.requested_setpoint == 34.2 + assert climate.requested_setpoint == 34.6 assert climate.pulse_width_modulation_enabled assert climate.pwm.last_duty_cycle_percentage == 100 From 039f1feaeb0b5a2c4a583b3584e71b0cf639fd3a Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 23 Nov 2024 18:21:38 +0100 Subject: [PATCH 092/213] Typo? --- custom_components/sat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index cb59199b..c012dc88 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -143,7 +143,7 @@ async def async_migrate_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool new_options["pid_controller_version"] = 1 if _entry.version < 8: - if _entry.options.get("heating_curve_version") is not None and _entry.options.get("heating_curve_version") < 2: + if _entry.options.get("heating_curve_version") is not None and int(_entry.options.get("heating_curve_version")) < 2: new_options["heating_curve_version"] = 3 _entry.version = SatFlowHandler.VERSION From 7f012efd456e59a36e7ebfb42e5b6698cc95a913 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 24 Nov 2024 12:49:38 +0100 Subject: [PATCH 093/213] Fix "division by zero" --- custom_components/sat/pwm.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index 73f684b7..60575461 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -110,8 +110,11 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) if not self._automatic_duty_cycle: return int(self._last_duty_cycle_percentage * self._max_cycle_time), int((1 - self._last_duty_cycle_percentage) * self._max_cycle_time) - if self._last_duty_cycle_percentage < MIN_DUTY_CYCLE_PERCENTAGE and boiler.flame_active and not boiler.hot_water_active: - return 180, 1620 + if self._last_duty_cycle_percentage < MIN_DUTY_CYCLE_PERCENTAGE: + if boiler.flame_active and not boiler.hot_water_active: + return 180, 1620 + + return 0, 1800 if self._last_duty_cycle_percentage <= DUTY_CYCLE_20_PERCENT: on_time = ON_TIME_20_PERCENT @@ -134,8 +137,6 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) if self._last_duty_cycle_percentage > MAX_DUTY_CYCLE_PERCENTAGE: return 1800, 0 - return 0, 1800 - @property def state(self) -> PWMState: """Returns the current state of the PWM control.""" From f75b4d9471cb92f0ce4e323136c401ee1f6474ac Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 27 Nov 2024 20:51:57 +0100 Subject: [PATCH 094/213] Improved PWM Duty Cycle --- README.md | 296 +++++++++++++------ custom_components/sat/__init__.py | 10 +- custom_components/sat/boiler_state.py | 2 +- custom_components/sat/climate.py | 2 +- custom_components/sat/config_flow.py | 33 ++- custom_components/sat/const.py | 2 + custom_components/sat/esphome/__init__.py | 2 +- custom_components/sat/pid.py | 2 +- custom_components/sat/pwm.py | 106 ++++--- custom_components/sat/relative_modulation.py | 2 +- custom_components/sat/translations/en.json | 2 + custom_components/sat/util.py | 3 +- tests/test_climate.py | 8 +- 13 files changed, 320 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index 38d2e042..8e152127 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Smart Autotune Thermostat +# Smart Autotune Thermostat (SAT) [![hacs][hacs-badge]][hacs-url] [![release][release-badge]][release-url] @@ -7,141 +7,251 @@ -![opentherm-mqtt.png](https://raw.githubusercontent.com/Alexwijn/SAT/develop/.github/images/opentherm-mqtt.png) -![overshoot_protection.png](https://raw.githubusercontent.com/Alexwijn/SAT/develop/.github/images/overshoot_protection.png) +![OpenTherm MQTT Integration](https://raw.githubusercontent.com/Alexwijn/SAT/develop/.github/images/opentherm-mqtt.png) +![Overshoot Protection Graph](https://raw.githubusercontent.com/Alexwijn/SAT/develop/.github/images/overshoot_protection.png) +## Overview -## What is the Smart Autotune Thermostat? - -The Smart Autotune Thermostat, or SAT for short, is a custom component for Home Assistant that works with an [OpenTherm Gateway (OTGW)](https://otgw.tclcode.com/) (MQTT or Serial). It can also function as a PID ON/OFF thermostat, providing advanced temperature control based on Outside Temperature compensation and the Proportional-Integral-Derivative (PID) algorithm. Unlike other thermostat components, SAT supports automatic gain tuning and heating curve coefficients. This capability allows it to determine the optimal setpoint for your boiler without any manual intervention. +The **Smart Autotune Thermostat (SAT)** is a custom component for [Home Assistant][home-assistant] designed to optimize your heating system's performance. It integrates with an [OpenTherm Gateway (OTGW)](https://otgw.tclcode.com/) via MQTT or Serial connection, and can also function as a PID ON/OFF thermostat. SAT provides advanced temperature control using Outside Temperature Compensation and Proportional-Integral-Derivative (PID) algorithms. Unlike other thermostat components, SAT supports automatic gain tuning and heating curve coefficients, allowing it to determine the optimal setpoint for your boiler without manual intervention. ## Features -OpenTherm ( MQTT / Serial ): -- Multi-room temperature control with support for temperature synchronization for main climates -- Overshoot protection value automatic calculation mechanism -- Adjustable heating curve coefficients to fine-tune your heating system -- Target temperature step for adjusting the temperature in smaller increments -- Presets for different modes such as Away, Sleep, Home, Comfort -- Automatic gains for PID control -- PWM and Automatic-duty cycle -- Overshoot protection to prevent the boiler from overshooting the setpoint ( Low-Load Control ) -- Climate valve offset to adjust the temperature reading for your climate valve -- Sample time for PID control to fine-tune your system's response time -- Open Window detection -- Control DHW setpoint - -PID ON/OFF thermostat: - -- Multi-room temperature control with support for temperature synchronization for main climates -- Adjustable heating curve coefficients to fine-tune your heating system -- Target temperature step for adjusting the temperature in smaller increments -- Presets for different modes such as Away, Sleep, Home, Comfort -- Automatic gains for PID control -- PWM and Automatic-duty cycle -- Climate valve offset to adjust the temperature reading for your climate valve -- Sample time for PID control to fine-tune your system's response time -- Open Window detection + +- Multi-room temperature control with support for temperature synchronization for main climates. +- Adjustable heating curve coefficients to fine-tune your heating system. +- Target temperature step for adjusting the temperature in smaller increments. +- Presets for different modes such as Away, Sleep, Home, Comfort. +- Automatic gains for PID control. +- PWM and automatic duty cycle. +- Climate valve offset to adjust the temperature reading for your climate valve. +- Sample time for PID control to fine-tune your system's response time. +- Open window detection. + +### OpenTherm-Specific Features + +- Overshoot protection value automatic calculation mechanism. +- Overshoot protection to prevent the boiler from overshooting the setpoint (Low-Load Control). +- Control Domestic Hot Water (DHW) setpoint. ## Installation -### Manual -1. Download the latest release of the SAT custom component from the GitHub repository. -2. Copy the sat directory to the custom_components directory in your Home Assistant configuration directory. If the custom_components directory doesn't exist, create it. + +### HACS + +Smart Autotune Thermostat (SAT) is available in [HACS][hacs] (Home Assistant Community Store). + +Use this link to directly go to the repository in HACS: + +[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=Alexwijn&repository=SAT) + +**Or follow these steps:** + +1. Install HACS if you don't have it already. +2. Open HACS in Home Assistant. +3. Search for **Smart Autotune Thermostat**. +4. Click the **Download** button. ⬇️ + +### Manual Installation + +1. Download the latest release of the SAT custom component from the [GitHub repository][release-url]. +2. Copy the `sat` directory to the `custom_components` directory in your Home Assistant configuration directory. If the `custom_components` directory doesn't exist, create it. 3. Restart Home Assistant to load the SAT custom component. 4. After installing the SAT custom component, you can configure it via the Home Assistant Config Flow interface. -### HACS -1. Install HACS if you haven't already. -2. Open the HACS web interface in Home Assistant and navigate to the Integrations section. -3. Click the three dots in the top-right corner and select "Custom repositories." -4. Enter the URL of the SAT custom component GitHub repository (https://github.com/Alexwijn/SAT) and select "Integration" as the category. Click "Add." -5. Once the SAT custom component appears in the list of available integrations, click "Install" to install it. -6. Restart Home Assistant to load the SAT custom component. -7. After installing the SAT custom component, you can configure it via the Home Assistant Config Flow interface. +## Configuration + +SAT is configured using a config flow. After installation, go to the **Integrations** page in Home Assistant, click on the **Add Integration** button, and search for **SAT** if the autodiscovery feature fails. + +### OpenTherm Configuration + +1. **OpenTherm Connection** + + - **MQTT**: + - **Name of the thermostat** + - **Top Topic** (*MQTT Top Topic* found in OTGW-firmware Settings) + - **Device** + + - **Serial**: + - **Name of the thermostat** + - **URL** + +2. **Configure Sensors** + + - **Inside Temperature Sensor** (Your room temperature sensor) + - **Outside Temperature Sensor** (Your outside temperature sensor) + - **Inside Humidity Sensor** (Your room humidity sensor) + +3. **Heating System** + + Selecting the correct heating system type is crucial for SAT to accurately control the temperature and optimize performance. Choose the option that matches your setup (e.g., Radiators or Underfloor heating) to ensure proper temperature regulation throughout your home. + +4. **Multi-Room Setup** + + > **Note:** If SAT is the only climate entity, skip this step. + + - **Primary:** You can add your physical thermostat. SAT will synchronize the `hvac_action` of the physical thermostat with the SAT climate entity's `hvac_action`. Additionally, the physical thermostat will act as a backup if any failure to Home Assistant occurs. + - **Rooms:** You can add your TRV (Thermostatic Radiator Valve) climate entities. When any of the rooms request heating, SAT will start the boiler. + + > **Tip:** Refer to the **Heating Mode** setting in the **General** tab for further customization. + +5. **Calibrate System** + + Optimize your heating system by automatically determining the optimal PID values for your setup. When selecting **Automatic Gains**, please note that the system will go through a calibration process that may take approximately 20 minutes to complete. + + If you already know this value, use the **Manually enter the overshoot protection value** option and enter the value. + + Automatic Gains are recommended for most users as it simplifies the setup process and ensures optimal performance. However, if you're familiar with PID control and prefer to manually set the values, you can choose to skip Automatic Gains. + + > **Note:** Choosing to skip Automatic Gains requires a good understanding of PID control and may require additional manual adjustments to achieve optimal performance. + +### PID ON/OFF Thermostat Configuration + +_To be completed._ + +## Settings + +### General Tab + +**Heating Curve Version** + +Represents the formulas used for calculating the heating curve. The available options are: + +- **Radiators**: + - [Classic Curve](https://www.desmos.com/calculator/cy8gjiciny) + - [Quantum Curve](https://www.desmos.com/calculator/hmrlrapnxz) + - [Precision Curve](https://www.desmos.com/calculator/spfvsid4ds) (**Recommended**) + +- **Underfloor Heating**: + - [Classic Curve](https://www.desmos.com/calculator/exjth5qsoe) + - [Quantum Curve](https://www.desmos.com/calculator/ke69ywalcz) + - [Precision Curve](https://www.desmos.com/calculator/i7f7uuyaoz) (**Recommended**) + +> **Note:** Graph parameters: +> +> - `a`: Heating Curve Coefficient +> - `b`: Room Setpoint + +> **Tip:** You can add the graph as an `iframe` card in Home Assistant for easy reference. + +**Example:** + +```yaml +type: iframe +url: https://www.desmos.com/calculator/spfvsid4ds +allow_open_top_navigation: true +allow: fullscreen +aspect_ratio: 130% +``` + +**PID Controller Version** + +- **Classic Controller** +- **Improved Controller** + +**Heating Mode** + +> **Note:** Available only for multi-room installations. + +- **Comfort:** SAT monitors the climates in other rooms to determine the error. It selects the highest error value as the PID error value for the current room. +- **Eco:** SAT monitors **only** the main thermostat's error, which is used as the PID error. + +**Maximum Setpoint** + +Set the maximum water setpoint for your system. + +- **Radiators:** Recommended to choose a value between 55–75 °C. Higher values will cause a more aggressive warm-up. +- **Underfloor Heating:** Recommended maximum water setpoint is 50 °C. + +**Heating Curve Coefficient** + +Adjust the heating curve coefficient to balance the heating loss of your home with the energy generated from your boiler based on the outside temperature. Proper tuning ensures the room temperature hovers around the setpoint. + +**Automatic Gains Value** + +Automatically tweak the aggressiveness of the PID gains (`kP`, `kI`, and `kD` values). Best results are achieved when using the same value as the Heating Curve Coefficient. + +**Derivative Time Weight** + +Further tweak the `kD` value. A good starting value is `2`. + +**Adjustment Factor for Return Temperature** + +This factor adjusts the heating setpoint based on the boiler's return temperature, affecting heating responsiveness and efficiency. A higher value increases sensitivity to temperature changes, enhancing control over comfort and energy use. + +> **Tip:** Recommended starting range is `0.1` to `0.5`. Adjust to suit your system and comfort preferences. + +**Contact Sensor** + +Add contact sensors (e.g., door/window sensors) to avoid wasting energy when a door/window is open. When the door/window is closed again, SAT restores heating. + +### Presets Tab + +Predefined temperature settings for different scenarios or activities, such as Away, Sleep, Home, and Comfort. + +### Advanced Tab -# Configuration -SAT is configured using a config flow. After installation, go to the Integrations page in Home Assistant, click on the Add Integration button, and search for SAT if the autodiscovery feature fails. +**Thermal Comfort** -## OpenTherm +Uses the Summer Simmer Index as the temperature sensor. The Summer Simmer Index refers to the perceived temperature based on the measured air temperature and relative humidity. -1. OpenTherm Connection - - MQTT - - Name of the thermostat - - Top Topic ( *MQTT Top Topic* found in OTGW-firmware Settings ) - - Device +**Dynamic Minimum Setpoint (Experimental)** - - Serial: - - Name of the thermostat - - URL +In multi-room installations, the boiler flow water temperature may exceed the Overshoot Protection Value during Low-Load Control (some valves may be closed). This mechanism monitors the boiler return water temperature and adjusts the Control Setpoint sent to the boiler accordingly. See **Adjustment Factor for Return Temperature**. -2. Configure sensors: - - Inside Sensor Entity ( Your Room Temperature sensor ) - - Outside temperature sensor ( Your Outside Temperature sensor ) +**Minimum Consumption** -3. Heating System: Selecting the correct heating system type is important for SAT to accurately control the temperature and optimize performance. Choose the option that matches your setup to ensure proper temperature regulation throughout your home. +Find this value in your boiler's manual. SAT uses this value to calculate the instant gas consumption. -4. Calibrate System: Optimize your heating system by automatically determining the optimal PID values for your setup. When selecting Automatic Gains, please note that the system will go through a calibration process that may take approximately 20 minutes to complete. +**Maximum Consumption** -If you already know this value, then use the "Manually enter the overshoot protection value" option and fill the value. +Find this value in your boiler's manual. SAT uses this value to calculate the instant gas consumption. -Automatic Gains are recommended for most users as it simplifies the setup process and ensures optimal performance. However, if you're familiar with PID control and prefer to manually set the values, you can choose to skip Automatic Gains. +**Target Temperature Step** -Please note that choosing to skip Automatic Gains requires a good understanding of PID control and may require additional manual adjustments to achieve optimal performance. +Adjusts the SAT climate entity room setpoint step. -## PID ON/OFF +**Maximum Relative Modulation** -To be completed +Control the maximum relative modulation at which the boiler will operate. -# Configure +## Terminology -## General tab: -*Maximum Setpoint*: -You can choose the max water setpoint for your system. -For radiator installations, it is recommended to choose a value between 55-75 °C. -For underfloor installations, the recommended max water setpoint is 50 °C. +**Heating Curve Coefficient** -Note for Radiators: Higher Max water setpoint values will cause a more aggressive warm-up. +By adjusting the heating curve coefficient, you can balance the heating loss of your home with the energy generated from your boiler at a given setpoint based on the outside temperature. Proper tuning ensures the room temperature hovers around the setpoint. -*Heating Curve Coefficient*: -The heating curve coefficient is a configurable parameter in SAT that allows you to adjust the relationship between the outdoor temperature and the heating system output. This is useful for optimizing the heating system's performance in different weather conditions, as it allows you to adjust how much heat the system delivers as the outdoor temperature changes. By tweaking this parameter, you can achieve a more efficient and comfortable heating system. +**PID Gains** -## Areas tab: -*Multi-room setup*: -In multi-room mode, SAT monitors the climates in other rooms to determine the error and calculates how much heat is needed. It selects the highest error value as the error value for the current room, instead of using the average temperature across all rooms. This ensures that the temperature in each room is maintained at its desired level. +SAT offers two ways of tuning the PID gains: -Note that SAT assumes that the climate control systems in the additional rooms are smart and won't exceed their target temperatures, as this can cause inefficiencies in the overall system. Once every climate control system in all rooms is around the target temperature, SAT can operate at its most efficient level. +- **Manual Tuning:** Fill the Proportional (`kP`), Integral (`kI`), and Derivative (`kD`) fields in the General tab with your values. +- **Automatic Gains (Recommended):** Enabled by default when the Overshoot Protection Value is present (during initial configuration). Automatic gains dynamically change the `kP`, `kI`, and `kD` values based on the heating curve value. This means that, based on the outside temperature, the gains change from mild to aggressive without intervention. -*Contact Sensor*: You can add contact sensors to avoid wasting energy when a door/window is open. When the door/window is closed again, SAT restores heating. +**Overshoot Protection** -## Presets tab: -Predefined temperature settings for different scenarios or activities. +This feature should be enabled when the minimum boiler capacity is greater than the control setpoint calculated by SAT. If the boiler overshoots the control setpoint, it may cycle, shortening the life of the burner. The solution is to adjust the boiler's on/off times to maintain the temperature at the setpoint while minimizing cycling. -# Terminology -*Heating Curve Coefficient*: By adjusting the heating curve coefficient, you can balance the heating loss of your home with the energy generated from your boiler at a given setpoint based on the outside temperature. When this value is properly tuned, the room temperature should hover around the setpoint. +**Overshoot Protection Value (OPV) Calculation** -*Gains*: SAT offers two ways of tuning the PID gains - manual and automatic. +The OPV is a crucial value that determines the boiler's on/off times when the Overshoot Protection feature is present (during initial configuration). -- Manual tuning: You can fill the Proportional, Integral, and Derivative fields in the General tab with your values. -- Automatic Gains ( Recommended ): This option is enabled by default when the Overshoot protection value is present (During initial configuration). Automatic gains dynamically change the kP, kI, and kD values based on the heating curve value. So, based on the outside temperature, the gains change from mild to aggressive without intervention. +- **Automatic Calculation:** To calculate the OPV automatically, choose the **Calibrate and determine your overshoot protection value (approx. 20 min)** option during the initial configuration. SAT will then send the `MM=0` and `CS=75` commands, attempting to find the highest flow water temperature the boiler can produce while running at 0% modulation. This process takes at least 20 minutes. Once the OPV calculation is complete, SAT will resume normal operation and send a completion notification. The calculated value will be stored as an attribute in the SAT climate entity and used to determine the boiler's on/off times in the low-load control algorithm. If SAT detects that the boiler doesn't respect the 0% Max Modulation command, it will automatically change the calibration algorithm to a more sophisticated one to perform the calibration of the system. -*Overshoot Protection*: This feature should be enabled when the minimum boiler capacity is greater than the control setpoint calculated by SAT. If the boiler overshoots the control setpoint, it may cycle, shortening the life of the burner. The solution is to adjust the boiler's on/off times to maintain the temperature at the setpoint while minimizing cycling. +- **Manual Calculation:** If you know the maximum flow water temperature of the boiler at 0% modulation, you can fill in this value during the initial configuration. -Overshoot Protection Value (OPV) Calculation: -The OPV is a crucial value that determines the boiler's on/off times when the Overshoot Protection feature is present (During initial configuration). +> **Note:** If you have any TRVs, open all of them (set them to a high setpoint) to ensure accurate calculation of the OPV. Once the calculation is complete, you can lower the setpoint back to your desired temperature. -*Manual Calculation*: If you know the maximum flow water temperature of the boiler at 0% modulation, you can fill in this value during the initial configuration. +**Automatic Duty Cycle** -*Automatic Calculation*: To calculate the OPV automatically, choose the "Calibrate and determine your overshoot protection value (approx. 20 min)" option during the initial configuration. SAT will then send the MM=0 and CS=75 commands, attempting to find the highest flow water temperature the boiler can produce while running at 0% modulation. This process takes at least 20 minutes. Once the OPV calculation is complete, SAT will resume normal operation and send a completion notification. The calculated value will be stored as an attribute in the SAT climate entity and used to determine the boiler's on/off times in the low-load control algorithm. If SAT detects that the boiler doesn't respect the 0% Max Modulation command, it will automatically change the calibration algorithm to a more sophisticated one to perform the calibration of the system. +When this option is enabled, SAT calculates the ON and OFF times of the boiler in 15-minute intervals, given that the kW needed to heat the home is less than the minimum boiler capacity. Additionally, using this feature, SAT can efficiently regulate the room temperature even in mild weather by automatically extending the duty cycle up to 30 minutes. -Note: If you have any TRVs, open all of them (set them to a high setpoint) to ensure accurate calculation of the OPV. Once the calculation is complete, you can lower the setpoint back to your desired temperature. +> **Tip:** For a more in-depth review of SAT and real-time observations, you can read this [excellent discussion post](https://github.com/Alexwijn/SAT/discussions/40) from [@critictidier](https://github.com/critictidier). -*Automatic Duty Cycle*: When this option is enabled, SAT calculates the ON and OFF times of the boiler in 15-minute intervals, given that the kW needed to heat the home is less than the minimum boiler capacity. Moreover, using this feature, SAT can efficiently regulate the room temperature even in mild weather by automatically adjusting the duty cycle. +--- [hacs-url]: https://github.com/hacs/integration [hacs-badge]: https://img.shields.io/badge/hacs-default-orange.svg?style=for-the-badge [release-badge]: https://img.shields.io/github/v/tag/Alexwijn/SAT?style=for-the-badge -[downloads-badge]: https://img.shields.io/github/downloads/Alexwijn/SAT/total?style=for-the-badge [build-badge]: https://img.shields.io/github/actions/workflow/status/Alexwijn/SAT/pytest.yml?branch=develop&style=for-the-badge [discord-badge]: https://img.shields.io/discord/1184879273991995515?label=Discord&logo=discord&logoColor=white&style=for-the-badge @@ -150,4 +260,4 @@ Note: If you have any TRVs, open all of them (set them to a high setpoint) to en [hacs]: https://hacs.xyz [home-assistant]: https://www.home-assistant.io/ [release-url]: https://github.com/Alexwijn/SAT/releases -[discord-url]: https://discord.gg/jnVXpzqGEq \ No newline at end of file +[discord-url]: https://discord.gg/jnVXpzqGEq diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index c012dc88..aaf7cea9 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -146,8 +146,14 @@ async def async_migrate_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool if _entry.options.get("heating_curve_version") is not None and int(_entry.options.get("heating_curve_version")) < 2: new_options["heating_curve_version"] = 3 - _entry.version = SatFlowHandler.VERSION - _hass.config_entries.async_update_entry(_entry, data=new_data, options=new_options) + if _entry.version < 9: + if _entry.data.get("heating_system") == "heat_pump": + new_options["cycles_per_hour"] = 2 + + if _entry.data.get("heating_system") == "radiators": + new_options["cycles_per_hour"] = 3 + + _hass.config_entries.async_update_entry(_entry, version=SatFlowHandler.VERSION, data=new_data, options=new_options) _LOGGER.info("Migration to version %s successful", _entry.version) diff --git a/custom_components/sat/boiler_state.py b/custom_components/sat/boiler_state.py index 2c901ff7..23ef367a 100644 --- a/custom_components/sat/boiler_state.py +++ b/custom_components/sat/boiler_state.py @@ -16,7 +16,7 @@ def __init__( :param device_active: Whether the boiler is currently operational. :param flame_active: Whether the boiler's flame is ignited. :param hot_water_active: Whether the boiler is heating water. - :param temperature: The current boiler temperature in degrees Celsius. + :param temperature: The current boiler temperature in Celsius. """ self._device_active = device_active self._flame_active = flame_active diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index dba83560..18d6e8f6 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -798,7 +798,7 @@ async def _async_control_pid(self, reset: bool = False) -> None: async def _async_control_setpoint(self, pwm_state: PWMState) -> None: """Control the setpoint of the heating system.""" if self.hvac_mode == HVACMode.HEAT: - if not self.pulse_width_modulation_enabled or pwm_state == pwm_state.IDLE: + if not self.pulse_width_modulation_enabled or pwm_state == pwm_state.IDLE or self.max_error < -DEADBAND: _LOGGER.info("Running Normal cycle") self._setpoint = self._calculated_setpoint else: diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 4ef54bfe..d87cea34 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -36,7 +36,7 @@ class SatFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for SAT.""" - VERSION = 8 + VERSION = 9 MINOR_VERSION = 0 calibration = None @@ -564,14 +564,33 @@ async def async_step_system_configuration(self, _user_input: dict[str, Any] | No options = await self.get_options() + schema = { + vol.Required(CONF_AUTOMATIC_DUTY_CYCLE, default=options[CONF_AUTOMATIC_DUTY_CYCLE]): bool, + vol.Required(CONF_SYNC_CLIMATES_WITH_MODE, default=options[CONF_SYNC_CLIMATES_WITH_MODE]): bool, + } + + if options.get(CONF_HEATING_SYSTEM) == HEATING_SYSTEM_HEAT_PUMP: + schema[vol.Required(CONF_CYCLES_PER_HOUR, default=str(options[CONF_CYCLES_PER_HOUR]))] = selector.SelectSelector( + selector.SelectSelectorConfig(mode=SelectSelectorMode.DROPDOWN, options=[ + selector.SelectOptionDict(value="2", label="Normal (2x per hour)"), + selector.SelectOptionDict(value="3", label="High (3x per hour)"), + ]) + ) + + if options.get(CONF_HEATING_SYSTEM) == HEATING_SYSTEM_RADIATORS: + schema[vol.Required(CONF_CYCLES_PER_HOUR, default=str(options[CONF_CYCLES_PER_HOUR]))] = selector.SelectSelector( + selector.SelectSelectorConfig(mode=SelectSelectorMode.DROPDOWN, options=[ + selector.SelectOptionDict(value="3", label="Normal (3x per hour)"), + selector.SelectOptionDict(value="4", label="High (4x per hour)"), + ]) + ) + + schema[vol.Required(CONF_SENSOR_MAX_VALUE_AGE, default=options[CONF_SENSOR_MAX_VALUE_AGE])] = selector.TimeSelector() + schema[vol.Required(CONF_WINDOW_MINIMUM_OPEN_TIME, default=options[CONF_WINDOW_MINIMUM_OPEN_TIME])] = selector.TimeSelector() + return self.async_show_form( step_id="system_configuration", - data_schema=vol.Schema({ - vol.Required(CONF_AUTOMATIC_DUTY_CYCLE, default=options[CONF_AUTOMATIC_DUTY_CYCLE]): bool, - vol.Required(CONF_SYNC_CLIMATES_WITH_MODE, default=options[CONF_SYNC_CLIMATES_WITH_MODE]): bool, - vol.Required(CONF_SENSOR_MAX_VALUE_AGE, default=options[CONF_SENSOR_MAX_VALUE_AGE]): selector.TimeSelector(), - vol.Required(CONF_WINDOW_MINIMUM_OPEN_TIME, default=options[CONF_WINDOW_MINIMUM_OPEN_TIME]): selector.TimeSelector(), - }) + data_schema=vol.Schema(schema) ) async def async_step_advanced(self, _user_input: dict[str, Any] | None = None): diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index 2f5b0d62..384ebdeb 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -27,6 +27,7 @@ CONF_MODE = "mode" CONF_NAME = "name" CONF_DEVICE = "device" +CONF_CYCLES_PER_HOUR = "cycles_per_hour" CONF_SIMULATED_HEATING = "simulated_heating" CONF_SIMULATED_COOLING = "simulated_cooling" CONF_SIMULATED_WARMING_UP = "simulated_warming_up" @@ -93,6 +94,7 @@ CONF_INTEGRAL: "0", CONF_DERIVATIVE: "6000", + CONF_CYCLES_PER_HOUR: 4, CONF_AUTOMATIC_GAINS: True, CONF_AUTOMATIC_DUTY_CYCLE: True, CONF_AUTOMATIC_GAINS_VALUE: 2.0, diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index c707c12c..544b8054 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -229,7 +229,7 @@ def _get_entity_id(self, domain: str, key: str): _LOGGER.debug(f"Attempting to find the unique_id of {unique_id}") return self._entity_registry.async_get_entity_id(domain, ESPHOME_DOMAIN, unique_id) - async def _send_command(self, domain: str, service: str, key: str, payload: dict): + async def _send_command(self, domain: str, service: str, _key: str, payload: dict): """Helper method to send a command to a specified domain and service.""" if not self._simulation: await self.hass.services.async_call(domain, service, payload, blocking=True) diff --git a/custom_components/sat/pid.py b/custom_components/sat/pid.py index b13c66fe..dc4c4b11 100644 --- a/custom_components/sat/pid.py +++ b/custom_components/sat/pid.py @@ -184,7 +184,7 @@ def update_derivative(self, error: float, alpha1: float = 0.8, alpha2: float = 0 if len(self._errors) < 2: return - # If derivative is disabled, we freeze it + # If "derivative" is disabled, we freeze it if not self.derivative_enabled: return diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index 60575461..10e841f8 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -11,8 +11,6 @@ DUTY_CYCLE_20_PERCENT = 0.2 DUTY_CYCLE_80_PERCENT = 0.8 -MIN_DUTY_CYCLE_PERCENTAGE = 0.1 -MAX_DUTY_CYCLE_PERCENTAGE = 0.9 ON_TIME_20_PERCENT = 180 ON_TIME_80_PERCENT = 900 @@ -25,58 +23,82 @@ class PWMState(str, Enum): class PWM: - """A class for implementing Pulse Width Modulation (PWM) control.""" + """Implements Pulse Width Modulation (PWM) control for managing boiler operations.""" - def __init__(self, heating_curve: HeatingCurve, max_cycle_time: int, automatic_duty_cycle: bool, force: bool = False): + def __init__(self, heating_curve: HeatingCurve, max_cycle_time: int, automatic_duty_cycle: bool, max_cycles: int, force: bool = False): """Initialize the PWM control.""" self._alpha = 0.2 self._force = force self._last_boiler_temperature = None - self._last_duty_cycle_percentage = None + self._max_cycles = max_cycles self._heating_curve = heating_curve self._max_cycle_time = max_cycle_time self._automatic_duty_cycle = automatic_duty_cycle + # Timing thresholds for duty cycle management + self._on_time_lower_threshold = 180 + self._on_time_higher_threshold = 3600 / self._max_cycles + self._on_time_max_threshold = self._on_time_higher_threshold * 2 + + # Duty cycle percentage thresholds + self._duty_cycle_lower_threshold = self._on_time_lower_threshold / self._on_time_higher_threshold + self._duty_cycle_max_threshold = 1 - self._duty_cycle_lower_threshold + self._min_duty_cycle_percentage = self._duty_cycle_lower_threshold / 2 + self._max_duty_cycle_percentage = 1 - self._min_duty_cycle_percentage + self.reset() def reset(self) -> None: """Reset the PWM control.""" + self._cycles = 0 self._duty_cycle = None self._state = PWMState.IDLE self._last_update = monotonic() + self._first_duty_cycle_start = None + self._last_duty_cycle_percentage = None + async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: """Update the PWM state based on the output of a PID controller.""" - if not self._heating_curve.value: - self._state = PWMState.IDLE - self._last_update = monotonic() - _LOGGER.warning("Turned off PWM due since we do not have a valid heating curve value.") - return - - if requested_setpoint is None: + if not self._heating_curve.value or requested_setpoint is None: self._state = PWMState.IDLE self._last_update = monotonic() self._last_boiler_temperature = boiler.temperature - _LOGGER.debug("Turned off PWM due since we do not have a valid requested setpoint.") + + reason = "heating curve value" if not self._heating_curve.value else "requested setpoint" + _LOGGER.warning(f"Turned off PWM due to lack of valid {reason}.") return if boiler.temperature is not None and self._last_boiler_temperature is None: self._last_boiler_temperature = boiler.temperature + if self._first_duty_cycle_start and (monotonic() - self._first_duty_cycle_start) > 3600: + self._cycles = 0 + self._first_duty_cycle_start = None + elapsed = monotonic() - self._last_update self._duty_cycle = self._calculate_duty_cycle(requested_setpoint, boiler) - _LOGGER.debug("Calculated duty cycle %.0f seconds ON", self._duty_cycle[0]) - _LOGGER.debug("Calculated duty cycle %.0f seconds OFF", self._duty_cycle[1]) + _LOGGER.debug("Calculated duty cycle %.0f seconds ON, %.0f seconds OFF", self._duty_cycle[0], self._duty_cycle[1]) + # Update boiler temperature if the heater has just started up if self._state == PWMState.ON and boiler.temperature is not None: if elapsed <= HEATER_STARTUP_TIMEFRAME: self._last_boiler_temperature = self._alpha * boiler.temperature + (1 - self._alpha) * self._last_boiler_temperature else: self._last_boiler_temperature = boiler.temperature + # State transitions for PWM if self._state != PWMState.ON and self._duty_cycle[0] >= HEATER_STARTUP_TIMEFRAME and (elapsed >= self._duty_cycle[1] or self._state == PWMState.IDLE): + if self._first_duty_cycle_start is None: + self._first_duty_cycle_start = monotonic() + + if self._cycles >= self._max_cycles: + _LOGGER.debug("Preventing duty cycle due to max cycles per hour.") + return + + self._cycles += 1 self._state = PWMState.ON self._last_update = monotonic() self._last_boiler_temperature = boiler.temperature or 0 @@ -91,31 +113,40 @@ async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: _LOGGER.debug("Cycle time elapsed %.0f seconds in %s", elapsed, self._state) - def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) -> Optional[Tuple[int, int]]: - """Calculates the duty cycle in seconds based on the output of a PID controller and a heating curve value.""" + def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) -> Tuple[int, int]: + """Calculate the duty cycle in seconds based on the output of a PID controller and a heating curve value.""" boiler_temperature = self._last_boiler_temperature or requested_setpoint base_offset = self._heating_curve.base_offset - if boiler_temperature <= base_offset: - boiler_temperature = base_offset + 1 + # Ensure boiler temperature is above the base offset + boiler_temperature = max(boiler_temperature, base_offset + 1) + # Calculate duty cycle percentage self._last_duty_cycle_percentage = (requested_setpoint - base_offset) / (boiler_temperature - base_offset) - self._last_duty_cycle_percentage = min(self._last_duty_cycle_percentage, 1) - self._last_duty_cycle_percentage = max(self._last_duty_cycle_percentage, 0) + self._last_duty_cycle_percentage = min(max(self._last_duty_cycle_percentage, 0), 1) _LOGGER.debug("Requested setpoint %.1f", requested_setpoint) _LOGGER.debug("Boiler Temperature %.1f", boiler_temperature) _LOGGER.debug("Calculated duty cycle %.2f%%", self._last_duty_cycle_percentage * 100) + # If automatic duty cycle control is disabled if not self._automatic_duty_cycle: - return int(self._last_duty_cycle_percentage * self._max_cycle_time), int((1 - self._last_duty_cycle_percentage) * self._max_cycle_time) + on_time = self._last_duty_cycle_percentage * self._max_cycle_time + off_time = (1 - self._last_duty_cycle_percentage) * self._max_cycle_time - if self._last_duty_cycle_percentage < MIN_DUTY_CYCLE_PERCENTAGE: + return int(on_time), int(off_time) + + # Handle special low-duty cycle cases + if self._last_duty_cycle_percentage < self._min_duty_cycle_percentage: if boiler.flame_active and not boiler.hot_water_active: - return 180, 1620 + on_time = self._on_time_lower_threshold + off_time = self._on_time_max_threshold - self._on_time_lower_threshold + + return int(on_time), int(off_time) - return 0, 1800 + return 0, int(self._on_time_max_threshold) + # Map duty cycle ranges to on/off times if self._last_duty_cycle_percentage <= DUTY_CYCLE_20_PERCENT: on_time = ON_TIME_20_PERCENT off_time = (ON_TIME_20_PERCENT / self._last_duty_cycle_percentage) - ON_TIME_20_PERCENT @@ -128,30 +159,29 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) return int(on_time), int(off_time) - if self._last_duty_cycle_percentage <= MAX_DUTY_CYCLE_PERCENTAGE: + if self._last_duty_cycle_percentage <= self._max_duty_cycle_percentage: on_time = ON_TIME_20_PERCENT / (1 - self._last_duty_cycle_percentage) - ON_TIME_20_PERCENT off_time = ON_TIME_20_PERCENT return int(on_time), int(off_time) - if self._last_duty_cycle_percentage > MAX_DUTY_CYCLE_PERCENTAGE: - return 1800, 0 + # Handle cases where the duty cycle exceeds the maximum allowed percentage + on_time = self._on_time_max_threshold + off_time = 0 + + return int(on_time), int(off_time) @property def state(self) -> PWMState: - """Returns the current state of the PWM control.""" + """Current PWM state.""" return self._state @property - def duty_cycle(self) -> None | tuple[int, int]: - """ - Returns the current duty cycle of the PWM control. - - If the PWM control is not currently active, None is returned. - Otherwise, a tuple is returned with the on and off times of the duty cycle in seconds. - """ + def duty_cycle(self) -> Optional[Tuple[int, int]]: + """Current duty cycle as a tuple of (on_time, off_time) in seconds, or None if inactive.""" return self._duty_cycle @property - def last_duty_cycle_percentage(self): - return round(self._last_duty_cycle_percentage * 100, 2) + def last_duty_cycle_percentage(self) -> Optional[float]: + """Returns the last calculated duty cycle percentage.""" + return round(self._last_duty_cycle_percentage * 100, 2) if self._last_duty_cycle_percentage is not None else None diff --git a/custom_components/sat/relative_modulation.py b/custom_components/sat/relative_modulation.py index e35062ff..103184a1 100644 --- a/custom_components/sat/relative_modulation.py +++ b/custom_components/sat/relative_modulation.py @@ -19,7 +19,7 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, heating_system: str): """Initialize instance variables""" self._heating_system = heating_system # The heating system that is being controlled self._pwm_state = None # Tracks the current state of the PWM (Pulse Width Modulation) system - self._warming_up = False # Stores data related to the warming up state of the heating system + self._warming_up = False # Stores data related to the warming-up state of the heating system self._coordinator = coordinator # Reference to the data coordinator responsible for system-wide information async def update(self, warming_up: bool, state: PWMState) -> None: diff --git a/custom_components/sat/translations/en.json b/custom_components/sat/translations/en.json index 6538b6fb..494213fc 100644 --- a/custom_components/sat/translations/en.json +++ b/custom_components/sat/translations/en.json @@ -210,6 +210,7 @@ }, "system_configuration": { "data": { + "cycles_per_hour": "Duty Cycles per hour", "automatic_duty_cycle": "Automatic duty cycle", "sync_climates_with_mode": "Synchronize climates with mode", "overshoot_protection": "Overshoot Protection (with PWM)", @@ -217,6 +218,7 @@ "window_minimum_open_time": "Minimum time for window to be open" }, "data_description": { + "cycles_per_hour": "The maximum amount of duty cycles per hour.", "automatic_duty_cycle": "Enable or disable automatic duty cycle for Pulse Width Modulation (PWM).", "overshoot_protection": "Enable overshoot protection with Pulse Width Modulation (PWM) to prevent boiler temperature overshooting.", "sensor_max_value_age": "The maximum age of the temperature sensor value before considering it as a stall.", diff --git a/custom_components/sat/util.py b/custom_components/sat/util.py index f0ab210f..c5f80532 100644 --- a/custom_components/sat/util.py +++ b/custom_components/sat/util.py @@ -80,12 +80,13 @@ def create_heating_curve_controller(config_data, config_options) -> HeatingCurve def create_pwm_controller(heating_curve: HeatingCurve, config_data, config_options) -> PWM | None: """Create and return a PWM controller instance with the given configuration options.""" # Extract the configuration options + max_duty_cycles = int(config_options.get(CONF_CYCLES_PER_HOUR)) automatic_duty_cycle = bool(config_options.get(CONF_AUTOMATIC_DUTY_CYCLE)) max_cycle_time = int(convert_time_str_to_seconds(config_options.get(CONF_DUTY_CYCLE))) force = bool(config_data.get(CONF_MODE) == MODE_SWITCH) or bool(config_options.get(CONF_FORCE_PULSE_WIDTH_MODULATION)) # Return a new PWM controller instance with the given configuration options - return PWM(heating_curve=heating_curve, max_cycle_time=max_cycle_time, automatic_duty_cycle=automatic_duty_cycle, force=force) + return PWM(heating_curve=heating_curve, max_cycle_time=max_cycle_time, automatic_duty_cycle=automatic_duty_cycle, max_cycles=max_duty_cycles, force=force) def create_minimum_setpoint_controller(config_data, config_options) -> MinimumSetpoint: diff --git a/tests/test_climate.py b/tests/test_climate.py index f21f2845..df006ad3 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -52,7 +52,7 @@ async def test_scenario_1(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.pulse_width_modulation_enabled assert climate.pwm.last_duty_cycle_percentage == 100 - assert climate.pwm.duty_cycle == (1800, 0) + assert climate.pwm.duty_cycle == (2400, 0) @pytest.mark.parametrize(*[ @@ -95,8 +95,8 @@ async def test_scenario_2(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.requested_setpoint == 28.0 assert climate.pulse_width_modulation_enabled - assert climate.pwm.last_duty_cycle_percentage == 100 - assert climate.pwm.duty_cycle == (1800, 0) + assert climate.pwm.last_duty_cycle_percentage == 80 + assert climate.pwm.duty_cycle == (720, 180) @pytest.mark.parametrize(*[ @@ -140,4 +140,4 @@ async def test_scenario_3(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.pulse_width_modulation_enabled assert climate.pwm.last_duty_cycle_percentage == 100 - assert climate.pwm.duty_cycle == (1800, 0) + assert climate.pwm.duty_cycle == (2400, 0) From 0caa90a4ee208cd5b551a24fdbabfd95ef5ee747 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 27 Nov 2024 21:45:08 +0100 Subject: [PATCH 095/213] Fixed some math problems --- custom_components/sat/pwm.py | 32 +++++++++++++------------------- tests/test_climate.py | 2 +- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index 10e841f8..d221ddd5 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -9,12 +9,6 @@ _LOGGER = logging.getLogger(__name__) -DUTY_CYCLE_20_PERCENT = 0.2 -DUTY_CYCLE_80_PERCENT = 0.8 - -ON_TIME_20_PERCENT = 180 -ON_TIME_80_PERCENT = 900 - class PWMState(str, Enum): ON = "on" @@ -38,12 +32,12 @@ def __init__(self, heating_curve: HeatingCurve, max_cycle_time: int, automatic_d # Timing thresholds for duty cycle management self._on_time_lower_threshold = 180 - self._on_time_higher_threshold = 3600 / self._max_cycles - self._on_time_max_threshold = self._on_time_higher_threshold * 2 + self._on_time_upper_threshold = 3600 / self._max_cycles + self._on_time_max_threshold = self._on_time_upper_threshold * 2 # Duty cycle percentage thresholds - self._duty_cycle_lower_threshold = self._on_time_lower_threshold / self._on_time_higher_threshold - self._duty_cycle_max_threshold = 1 - self._duty_cycle_lower_threshold + self._duty_cycle_lower_threshold = self._on_time_lower_threshold / self._on_time_upper_threshold + self._duty_cycle_upper_threshold = 1 - self._duty_cycle_lower_threshold self._min_duty_cycle_percentage = self._duty_cycle_lower_threshold / 2 self._max_duty_cycle_percentage = 1 - self._min_duty_cycle_percentage @@ -137,7 +131,7 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) return int(on_time), int(off_time) # Handle special low-duty cycle cases - if self._last_duty_cycle_percentage < self._min_duty_cycle_percentage: + if self._last_duty_cycle_percentage < self._duty_cycle_lower_threshold: if boiler.flame_active and not boiler.hot_water_active: on_time = self._on_time_lower_threshold off_time = self._on_time_max_threshold - self._on_time_lower_threshold @@ -147,21 +141,21 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) return 0, int(self._on_time_max_threshold) # Map duty cycle ranges to on/off times - if self._last_duty_cycle_percentage <= DUTY_CYCLE_20_PERCENT: - on_time = ON_TIME_20_PERCENT - off_time = (ON_TIME_20_PERCENT / self._last_duty_cycle_percentage) - ON_TIME_20_PERCENT + if self._last_duty_cycle_percentage <= self._duty_cycle_lower_threshold: + on_time = self._on_time_lower_threshold + off_time = (self._on_time_lower_threshold / self._last_duty_cycle_percentage) - self._on_time_lower_threshold return int(on_time), int(off_time) - if self._last_duty_cycle_percentage <= DUTY_CYCLE_80_PERCENT: - on_time = ON_TIME_80_PERCENT * self._last_duty_cycle_percentage - off_time = ON_TIME_80_PERCENT * (1 - self._last_duty_cycle_percentage) + if self._last_duty_cycle_percentage <= self._duty_cycle_upper_threshold: + on_time = self._on_time_upper_threshold * self._last_duty_cycle_percentage + off_time = self._on_time_upper_threshold * (1 - self._last_duty_cycle_percentage) return int(on_time), int(off_time) if self._last_duty_cycle_percentage <= self._max_duty_cycle_percentage: - on_time = ON_TIME_20_PERCENT / (1 - self._last_duty_cycle_percentage) - ON_TIME_20_PERCENT - off_time = ON_TIME_20_PERCENT + on_time = self._on_time_lower_threshold / (1 - self._last_duty_cycle_percentage) - self._on_time_lower_threshold + off_time = self._on_time_lower_threshold return int(on_time), int(off_time) diff --git a/tests/test_climate.py b/tests/test_climate.py index df006ad3..443bbe72 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -96,7 +96,7 @@ async def test_scenario_2(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.pulse_width_modulation_enabled assert climate.pwm.last_duty_cycle_percentage == 80 - assert climate.pwm.duty_cycle == (720, 180) + assert climate.pwm.duty_cycle == (720, 179) @pytest.mark.parametrize(*[ From 2c389b9b7c1f4dc90d36f0d428c21d1053cb3bb2 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 27 Nov 2024 21:45:30 +0100 Subject: [PATCH 096/213] Improved HVAC Mode OFF --- custom_components/sat/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 18d6e8f6..17fd7839 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -862,6 +862,9 @@ async def async_control_heating_loop(self, _time=None) -> None: if self.current_temperature is None or self.target_temperature is None or self.current_outside_temperature is None: return + if self.hvac_mode != HVACMode.HEAT: + return + # Control the heating through the coordinator await self._coordinator.async_control_heating_loop(self) @@ -903,7 +906,7 @@ async def async_control_heating_loop(self, _time=None) -> None: self._minimum_setpoint.calculate(self._coordinator.return_temperature) # If the setpoint is high and the HVAC is not off, turn on the heater - if self._setpoint > MINIMUM_SETPOINT and self.hvac_mode != HVACMode.OFF: + if self._setpoint > MINIMUM_SETPOINT: await self.async_set_heater_state(DeviceState.ON) else: await self.async_set_heater_state(DeviceState.OFF) @@ -935,8 +938,10 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: # Only allow the hvac mode to be set to heat or off if hvac_mode == HVACMode.HEAT: self._hvac_mode = HVACMode.HEAT + await self.async_set_heater_state(DeviceState.ON) elif hvac_mode == HVACMode.OFF: self._hvac_mode = HVACMode.OFF + await self.async_set_heater_state(DeviceState.OFF) else: # If an unsupported mode is passed, log an error message _LOGGER.error("Unrecognized hvac mode: %s", hvac_mode) From c2e9ec0ec4044c89afcbfdc9d7f633c15b16a940 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 27 Nov 2024 21:46:05 +0100 Subject: [PATCH 097/213] Make sure we turn off the existing climate when re-configuring --- custom_components/sat/config_flow.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index d87cea34..7cbd25a4 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries from homeassistant.components import mqtt from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ATTR_HVAC_MODE, HVACMode, SERVICE_SET_HVAC_MODE from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.input_boolean import DOMAIN as INPUT_BOOLEAN_DOMAIN from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN @@ -15,7 +15,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import MAJOR_VERSION, MINOR_VERSION +from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, ATTR_ENTITY_ID from homeassistant.core import callback from homeassistant.helpers import selector, device_registry, entity_registry from homeassistant.helpers.selector import SelectSelectorMode @@ -40,6 +40,7 @@ class SatFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): MINOR_VERSION = 0 calibration = None + previous_hvac_mode = None overshoot_protection_value = None def __init__(self): @@ -333,6 +334,11 @@ async def async_step_calibrate_system(self, _user_input: dict[str, Any] | None = async def async_step_calibrate(self, _user_input: dict[str, Any] | None = None): coordinator = await self.async_create_coordinator() + # Let's see if we have already been configured before + entities = entity_registry.async_get(self.hass) + device_name = self.config_entry.data.get(CONF_NAME) + climate_id = entities.async_get_entity_id(CLIMATE_DOMAIN, DOMAIN, device_name.lower()) + async def start_calibration(): try: overshoot_protection = OvershootProtection(coordinator, self.data.get(CONF_HEATING_SYSTEM)) @@ -355,6 +361,12 @@ async def start_calibration(): start_calibration() ) + # Make sure to turn off the existing climate if we found one + if climate_id is not None: + self.previous_hvac_mode = self.hass.states.get(climate_id).state + data = {ATTR_ENTITY_ID: climate_id, ATTR_HVAC_MODE: HVACMode.OFF} + await self.hass.services.async_call(CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, data, blocking=True) + return self.async_show_progress( step_id="calibrate", progress_task=self.calibration, @@ -371,6 +383,11 @@ async def start_calibration(): self.calibration = None self.overshoot_protection_value = None + # Make sure to restore the mode after we are done + if climate_id is not None: + data = {ATTR_ENTITY_ID: climate_id, ATTR_HVAC_MODE: self.previous_hvac_mode} + await self.hass.services.async_call(CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, data, blocking=True) + return self.async_show_progress_done(next_step_id="calibrated") async def async_step_calibrated(self, _user_input: dict[str, Any] | None = None): From 15de6a5563e7ddbe08b862c4db2f4248e4d12505 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 27 Nov 2024 21:48:01 +0100 Subject: [PATCH 098/213] No need to turn on the heater when it is controlled elsewhere --- custom_components/sat/climate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 17fd7839..5624f585 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -938,7 +938,6 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: # Only allow the hvac mode to be set to heat or off if hvac_mode == HVACMode.HEAT: self._hvac_mode = HVACMode.HEAT - await self.async_set_heater_state(DeviceState.ON) elif hvac_mode == HVACMode.OFF: self._hvac_mode = HVACMode.OFF await self.async_set_heater_state(DeviceState.OFF) From 1493e34be7f0d14cbe66dd9d671c08f5c1c95341 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 27 Nov 2024 21:50:10 +0100 Subject: [PATCH 099/213] Typo? --- tests/test_climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_climate.py b/tests/test_climate.py index 443bbe72..ea5c4811 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -96,7 +96,7 @@ async def test_scenario_2(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.pulse_width_modulation_enabled assert climate.pwm.last_duty_cycle_percentage == 80 - assert climate.pwm.duty_cycle == (720, 179) + assert climate.pwm.duty_cycle == (960, 239) @pytest.mark.parametrize(*[ From 77d9067acce4adb8fedb553872501c584c567570 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 27 Nov 2024 21:58:48 +0100 Subject: [PATCH 100/213] Make sure all climate valves are open when calibrating --- custom_components/sat/config_flow.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 7cbd25a4..63f7214d 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -367,6 +367,11 @@ async def start_calibration(): data = {ATTR_ENTITY_ID: climate_id, ATTR_HVAC_MODE: HVACMode.OFF} await self.hass.services.async_call(CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, data, blocking=True) + # Make sure all climate valves are open + for entity_id in self.data.get(CONF_MAIN_CLIMATES, []) + self.data.get(CONF_SECONDARY_CLIMATES, []): + data = {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.HEAT} + await self.hass.services.async_call(CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, data, blocking=True) + return self.async_show_progress( step_id="calibrate", progress_task=self.calibration, From 0fda41fe90867ab7b9ab8fa6b1c87dd5a1e3fbfa Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 27 Nov 2024 22:04:08 +0100 Subject: [PATCH 101/213] Make sure we keep sending the heater state --- custom_components/sat/overshoot_protection.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index f77b9f3d..3f2f28d9 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -22,8 +22,6 @@ async def calculate(self) -> float | None: try: _LOGGER.info("Starting overshoot protection calculation") - await self._coordinator.async_set_heater_state(DeviceState.ON) - # Enforce timeouts to ensure operations do not run indefinitely await asyncio.wait_for(self._wait_for_flame(), timeout=OVERSHOOT_PROTECTION_INITIAL_WAIT) await asyncio.wait_for(self._wait_for_stable_temperature(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) @@ -55,6 +53,8 @@ async def _wait_for_stable_temperature(self) -> None: previous_average_temperature = float(self._coordinator.boiler_temperature) while True: + await self._coordinator.async_set_heater_state(DeviceState.ON) + current_temperature = float(self._coordinator.boiler_temperature) average_temperature, error_value = self._calculate_exponential_moving_average(previous_average_temperature, current_temperature) @@ -70,6 +70,8 @@ async def _wait_for_stable_relative_modulation(self) -> float: previous_average_value = float(self._coordinator.relative_modulation_value) while True: + await self._coordinator.async_set_heater_state(DeviceState.ON) + current_value = float(self._coordinator.relative_modulation_value) average_value, error_value = self._calculate_exponential_moving_average(previous_average_value, current_value) From 836781a6bbd30724bc311684cbad41b8a2a9e9e5 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 27 Nov 2024 22:06:12 +0100 Subject: [PATCH 102/213] Make sure we control the heater mode properly --- custom_components/sat/coordinator.py | 5 ----- custom_components/sat/overshoot_protection.py | 5 +---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index 79848c70..72ef048f 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -6,7 +6,6 @@ from enum import Enum from typing import TYPE_CHECKING, Mapping, Any, Optional -from homeassistant.components.climate import HVACMode from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -255,10 +254,6 @@ async def async_will_remove_from_hass(self, climate: SatClimate) -> None: async def async_control_heating_loop(self, climate: SatClimate = None, _time=None) -> None: """Control the heating loop for the device.""" - if climate is not None and climate.hvac_mode == HVACMode.OFF and self.device_active: - # Send out a new command to turn off the device - await self.async_set_heater_state(DeviceState.OFF) - current_time = datetime.now() # Make sure we have valid value diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 3f2f28d9..56d02098 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -53,8 +53,6 @@ async def _wait_for_stable_temperature(self) -> None: previous_average_temperature = float(self._coordinator.boiler_temperature) while True: - await self._coordinator.async_set_heater_state(DeviceState.ON) - current_temperature = float(self._coordinator.boiler_temperature) average_temperature, error_value = self._calculate_exponential_moving_average(previous_average_temperature, current_temperature) @@ -70,8 +68,6 @@ async def _wait_for_stable_relative_modulation(self) -> float: previous_average_value = float(self._coordinator.relative_modulation_value) while True: - await self._coordinator.async_set_heater_state(DeviceState.ON) - current_value = float(self._coordinator.relative_modulation_value) average_value, error_value = self._calculate_exponential_moving_average(previous_average_value, current_value) @@ -94,6 +90,7 @@ def _calculate_exponential_moving_average(self, previous_average: float, current return average_value, error_value async def _trigger_heating_cycle(self) -> None: + await self._coordinator.async_set_heater_state(DeviceState.ON) await self._coordinator.async_set_control_setpoint(self._setpoint) await self._coordinator.async_set_control_max_relative_modulation(MAXIMUM_RELATIVE_MOD) await asyncio.sleep(SLEEP_INTERVAL) From c0ead2363fb6ffcfb98790d3024e0d8243088897 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 27 Nov 2024 22:13:56 +0100 Subject: [PATCH 103/213] Make sure we pass on the exception --- custom_components/sat/overshoot_protection.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 56d02098..248e2b28 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -32,9 +32,10 @@ async def calculate(self) -> float | None: relative_modulation_value = await asyncio.wait_for(self._wait_for_stable_relative_modulation(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) return self._calculate_overshoot_value(relative_modulation_value) - except asyncio.TimeoutError: + except asyncio.TimeoutError as exception: _LOGGER.warning("Timed out during overshoot protection calculation") - return None + + raise exception except asyncio.CancelledError as exception: _LOGGER.info("Calculation cancelled, shutting down heating system") await self._coordinator.async_set_heater_state(DeviceState.OFF) From 697461b9e3273921dd3be2e8f30fb323583e945c Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Thu, 28 Nov 2024 19:55:14 +0100 Subject: [PATCH 104/213] Fix some math issues --- custom_components/sat/pwm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index d221ddd5..724e6ccc 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -131,7 +131,7 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) return int(on_time), int(off_time) # Handle special low-duty cycle cases - if self._last_duty_cycle_percentage < self._duty_cycle_lower_threshold: + if self._last_duty_cycle_percentage < self._min_duty_cycle_percentage: if boiler.flame_active and not boiler.hot_water_active: on_time = self._on_time_lower_threshold off_time = self._on_time_max_threshold - self._on_time_lower_threshold From 2dd35d43a3a4cf24b0488cf133d0a18956b51b21 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 29 Nov 2024 22:14:51 +0100 Subject: [PATCH 105/213] Add some extra logging --- custom_components/sat/pwm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index 724e6ccc..361a69d8 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -70,11 +70,12 @@ async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: if self._first_duty_cycle_start and (monotonic() - self._first_duty_cycle_start) > 3600: self._cycles = 0 self._first_duty_cycle_start = None + _LOGGER.debug("Resetting CYCLES to zero, since an hour has passed.") elapsed = monotonic() - self._last_update self._duty_cycle = self._calculate_duty_cycle(requested_setpoint, boiler) - _LOGGER.debug("Calculated duty cycle %.0f seconds ON, %.0f seconds OFF", self._duty_cycle[0], self._duty_cycle[1]) + _LOGGER.debug("Calculated duty cycle %.0f seconds ON, %.0f seconds OFF, %d CYCLES this hour.", self._duty_cycle[0], self._duty_cycle[1], self._cycles) # Update boiler temperature if the heater has just started up if self._state == PWMState.ON and boiler.temperature is not None: From 7c4f9fd788a6a71a89ac55a39dd0813c4ecd54bf Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 29 Nov 2024 22:17:35 +0100 Subject: [PATCH 106/213] Add some extra logging --- custom_components/sat/pwm.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index 361a69d8..a35c61f1 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -122,7 +122,10 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) _LOGGER.debug("Requested setpoint %.1f", requested_setpoint) _LOGGER.debug("Boiler Temperature %.1f", boiler_temperature) + _LOGGER.debug("Calculated duty cycle %.2f%%", self._last_duty_cycle_percentage * 100) + _LOGGER.debug("Calculated duty cycle lower threshold %.2f%%", self._duty_cycle_lower_threshold * 100) + _LOGGER.debug("Calculated duty cycle upper threshold %.2f%%", self._duty_cycle_upper_threshold * 100) # If automatic duty cycle control is disabled if not self._automatic_duty_cycle: From 9a16d77d173ff933c956718610895ef37a23c815 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 29 Nov 2024 22:52:25 +0100 Subject: [PATCH 107/213] Make the soft limiter more explicit --- custom_components/sat/pwm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index a35c61f1..4201a62c 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -118,7 +118,8 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) # Calculate duty cycle percentage self._last_duty_cycle_percentage = (requested_setpoint - base_offset) / (boiler_temperature - base_offset) - self._last_duty_cycle_percentage = min(max(self._last_duty_cycle_percentage, 0), 1) + self._last_duty_cycle_percentage = min(self._last_duty_cycle_percentage, 1) + self._last_duty_cycle_percentage = max(self._last_duty_cycle_percentage, 0) _LOGGER.debug("Requested setpoint %.1f", requested_setpoint) _LOGGER.debug("Boiler Temperature %.1f", boiler_temperature) From cc01a61bd68dba88c79ba7d24e5c3e07c91a0dd7 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 11:05:34 +0100 Subject: [PATCH 108/213] Do note use the filtered boiler temperature --- custom_components/sat/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 5624f585..3bcc1532 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -881,7 +881,7 @@ async def async_control_heating_loop(self, _time=None) -> None: flame_active=self._coordinator.flame_active, device_active=self._coordinator.device_active, hot_water_active=self._coordinator.hot_water_active, - temperature=self._coordinator.filtered_boiler_temperature + temperature=self._coordinator.boiler_temperature ) await self.pwm.update(self._calculated_setpoint, boiler_state) From 465d1c12c391832f2120f8f0730794e1d09cccc8 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 11:08:15 +0100 Subject: [PATCH 109/213] Update tests --- tests/test_climate.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_climate.py b/tests/test_climate.py index ea5c4811..d407c330 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -51,8 +51,8 @@ async def test_scenario_1(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.heating_curve.value == 32.2 assert climate.pulse_width_modulation_enabled - assert climate.pwm.last_duty_cycle_percentage == 100 - assert climate.pwm.duty_cycle == (2400, 0) + assert climate.pwm.last_duty_cycle_percentage == 23.83 + assert climate.pwm.duty_cycle == (285, 914) @pytest.mark.parametrize(*[ @@ -90,13 +90,13 @@ async def test_scenario_2(hass: HomeAssistant, entry: MockConfigEntry, climate: await climate.async_set_target_temperature(19.0) await climate.async_set_hvac_mode(HVACMode.HEAT) - assert climate.setpoint == 58 + assert climate.setpoint == 10 assert climate.heating_curve.value == 27.8 assert climate.requested_setpoint == 28.0 assert climate.pulse_width_modulation_enabled - assert climate.pwm.last_duty_cycle_percentage == 80 - assert climate.pwm.duty_cycle == (960, 239) + assert climate.pwm.last_duty_cycle_percentage == 2.6 + assert climate.pwm.duty_cycle == (0, 2400) @pytest.mark.parametrize(*[ @@ -139,5 +139,5 @@ async def test_scenario_3(hass: HomeAssistant, entry: MockConfigEntry, climate: assert climate.requested_setpoint == 34.6 assert climate.pulse_width_modulation_enabled - assert climate.pwm.last_duty_cycle_percentage == 100 - assert climate.pwm.duty_cycle == (2400, 0) + assert climate.pwm.last_duty_cycle_percentage == 53.62 + assert climate.pwm.duty_cycle == (643, 556) From c1bec1d8aaa716d8308973ec22bba03a6af55de6 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 11:10:40 +0100 Subject: [PATCH 110/213] Return the current temperature if we do not have anything stored yet --- custom_components/sat/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index 72ef048f..aea5c80d 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -131,10 +131,10 @@ def return_temperature(self) -> float | None: return None @property - def filtered_boiler_temperature(self) -> float | None: + def filtered_boiler_temperature(self) -> float: # Not able to use if we do not have at least two values if len(self.boiler_temperatures) < 2: - return None + return self.boiler_temperature # Some noise filtering on the boiler temperature difference_boiler_temperature_sum = sum( From e5fd9cde565d0072b342260595dd3e5f1cdd2e9a Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 11:12:56 +0100 Subject: [PATCH 111/213] Some cleanup --- custom_components/sat/climate.py | 2 +- custom_components/sat/pwm.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 3bcc1532..5624f585 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -881,7 +881,7 @@ async def async_control_heating_loop(self, _time=None) -> None: flame_active=self._coordinator.flame_active, device_active=self._coordinator.device_active, hot_water_active=self._coordinator.hot_water_active, - temperature=self._coordinator.boiler_temperature + temperature=self._coordinator.filtered_boiler_temperature ) await self.pwm.update(self._calculated_setpoint, boiler_state) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index 4201a62c..a5aeeaee 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -64,7 +64,7 @@ async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: _LOGGER.warning(f"Turned off PWM due to lack of valid {reason}.") return - if boiler.temperature is not None and self._last_boiler_temperature is None: + if self._last_boiler_temperature is None: self._last_boiler_temperature = boiler.temperature if self._first_duty_cycle_start and (monotonic() - self._first_duty_cycle_start) > 3600: @@ -96,7 +96,7 @@ async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: self._cycles += 1 self._state = PWMState.ON self._last_update = monotonic() - self._last_boiler_temperature = boiler.temperature or 0 + self._last_boiler_temperature = boiler.temperature _LOGGER.debug("Starting duty cycle.") return @@ -110,7 +110,7 @@ async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) -> Tuple[int, int]: """Calculate the duty cycle in seconds based on the output of a PID controller and a heating curve value.""" - boiler_temperature = self._last_boiler_temperature or requested_setpoint + boiler_temperature = self._last_boiler_temperature base_offset = self._heating_curve.base_offset # Ensure boiler temperature is above the base offset From 2c96d753ca36dc6dc578b87f82c1688c54fcc866 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 12:03:33 +0100 Subject: [PATCH 112/213] Update some naming --- custom_components/sat/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/sensor.py b/custom_components/sat/sensor.py index 8b1b9ce7..da742420 100644 --- a/custom_components/sat/sensor.py +++ b/custom_components/sat/sensor.py @@ -54,7 +54,7 @@ class SatCurrentPowerSensor(SatEntity, SensorEntity): @property def name(self) -> str: - return f"Boiler Current Power {self._config_entry.data.get(CONF_NAME)} (Boiler)" + return f"Current Power {self._config_entry.data.get(CONF_NAME)} (Boiler)" @property def device_class(self): @@ -95,7 +95,7 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn @property def name(self) -> str: - return f"Boiler Current Consumption {self._config_entry.data.get(CONF_NAME)} (Boiler)" + return f"Current Consumption {self._config_entry.data.get(CONF_NAME)} (Boiler)" @property def device_class(self): From c772fb423b7646cffcf20eb3c8b797ea4346e66b Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 12:45:35 +0100 Subject: [PATCH 113/213] More cleanup --- custom_components/sat/pwm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index a5aeeaee..575a0c23 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -77,8 +77,7 @@ async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: _LOGGER.debug("Calculated duty cycle %.0f seconds ON, %.0f seconds OFF, %d CYCLES this hour.", self._duty_cycle[0], self._duty_cycle[1], self._cycles) - # Update boiler temperature if the heater has just started up - if self._state == PWMState.ON and boiler.temperature is not None: + if self._state == PWMState.ON: if elapsed <= HEATER_STARTUP_TIMEFRAME: self._last_boiler_temperature = self._alpha * boiler.temperature + (1 - self._alpha) * self._last_boiler_temperature else: From c797b0ad02cd9a90924f2c0c0748d8e621ae5597 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 12:50:17 +0100 Subject: [PATCH 114/213] Do not stay in normal cycle when we are near the deadband --- custom_components/sat/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 5624f585..627ec5a5 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -798,7 +798,7 @@ async def _async_control_pid(self, reset: bool = False) -> None: async def _async_control_setpoint(self, pwm_state: PWMState) -> None: """Control the setpoint of the heating system.""" if self.hvac_mode == HVACMode.HEAT: - if not self.pulse_width_modulation_enabled or pwm_state == pwm_state.IDLE or self.max_error < -DEADBAND: + if not self.pulse_width_modulation_enabled or pwm_state == pwm_state.IDLE: _LOGGER.info("Running Normal cycle") self._setpoint = self._calculated_setpoint else: From 592968ffd181f56f2c9de7e9ea176fac8be72fba Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 13:01:24 +0100 Subject: [PATCH 115/213] Add missing option in the config flow --- custom_components/sat/config_flow.py | 1 + custom_components/sat/translations/en.json | 1 + 2 files changed, 2 insertions(+) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 63f7214d..1c22d5c5 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -576,6 +576,7 @@ async def async_step_presets(self, _user_input: dict[str, Any] | None = None): vol.Required(CONF_COMFORT_TEMPERATURE, default=options[CONF_COMFORT_TEMPERATURE]): selector.NumberSelector( selector.NumberSelectorConfig(min=5, max=35, step=0.5, unit_of_measurement="°C") ), + vol.Required(CONF_SYNC_WITH_THERMOSTAT, default=options[CONF_SYNC_WITH_THERMOSTAT]): bool, vol.Required(CONF_SYNC_CLIMATES_WITH_PRESET, default=options[CONF_SYNC_CLIMATES_WITH_PRESET]): bool, }) ) diff --git a/custom_components/sat/translations/en.json b/custom_components/sat/translations/en.json index 494213fc..a8e0c915 100644 --- a/custom_components/sat/translations/en.json +++ b/custom_components/sat/translations/en.json @@ -203,6 +203,7 @@ "comfort_temperature": "Comfort Temperature", "home_temperature": "Home Temperature", "sleep_temperature": "Sleep Temperature", + "sync_with_thermostat": "Synchronize with thermostat attached to the boiler", "sync_climates_with_preset": "Synchronize climates with preset (sleep / away / activity)" }, "description": "Predefined temperature settings for different scenarios or activities.", From 5e357417745e01d5e91f4714fe5458794744a2d1 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 13:40:22 +0100 Subject: [PATCH 116/213] Always use the raw temperature --- custom_components/sat/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 627ec5a5..331814ec 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -881,7 +881,7 @@ async def async_control_heating_loop(self, _time=None) -> None: flame_active=self._coordinator.flame_active, device_active=self._coordinator.device_active, hot_water_active=self._coordinator.hot_water_active, - temperature=self._coordinator.filtered_boiler_temperature + temperature=self._coordinator.boiler_temperatures ) await self.pwm.update(self._calculated_setpoint, boiler_state) From 16154a66a91dae5e65771ee39f29e94b83487fdc Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 14:07:56 +0100 Subject: [PATCH 117/213] Typo? --- custom_components/sat/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 331814ec..ab7454bb 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -881,7 +881,7 @@ async def async_control_heating_loop(self, _time=None) -> None: flame_active=self._coordinator.flame_active, device_active=self._coordinator.device_active, hot_water_active=self._coordinator.hot_water_active, - temperature=self._coordinator.boiler_temperatures + temperature=self._coordinator.boiler_temperature ) await self.pwm.update(self._calculated_setpoint, boiler_state) From 6a959ae37293589924b79e24cf1b98b4377607cb Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 14:52:03 +0100 Subject: [PATCH 118/213] More cleaning up and add proper support for deadband in combination with PWM --- custom_components/sat/climate.py | 34 +++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index ab7454bb..41f428cf 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -796,19 +796,35 @@ async def _async_control_pid(self, reset: bool = False) -> None: self.async_write_ha_state() async def _async_control_setpoint(self, pwm_state: PWMState) -> None: - """Control the setpoint of the heating system.""" - if self.hvac_mode == HVACMode.HEAT: - if not self.pulse_width_modulation_enabled or pwm_state == pwm_state.IDLE: - _LOGGER.info("Running Normal cycle") - self._setpoint = self._calculated_setpoint - else: - _LOGGER.info(f"Running PWM cycle: {pwm_state}") - self._setpoint = self.minimum_setpoint if pwm_state == pwm_state.ON else MINIMUM_SETPOINT - else: + """Control the setpoint of the heating system based on the current mode and PWM state.""" + + # Check if the system is in HEAT mode + if self.hvac_mode != HVACMode.HEAT: + # If not in HEAT mode, set to minimum setpoint self._calculated_setpoint = None self._setpoint = MINIMUM_SETPOINT + _LOGGER.info("HVAC mode is not HEAT. Setting setpoint to minimum: %.1f°C", MINIMUM_SETPOINT) + + elif not self.pulse_width_modulation_enabled or pwm_state == PWMState.IDLE: + # Normal cycle without PWM + _LOGGER.info("Pulse Width Modulation is disabled or in IDLE state. Running normal heating cycle.") + _LOGGER.debug("Calculated setpoint for normal cycle: %.1f°C", self._calculated_setpoint) + self._setpoint = self._calculated_setpoint + + else: + # PWM is enabled and actively controlling the cycle + _LOGGER.info("Running PWM cycle with state: %s", pwm_state) + + if pwm_state == PWMState.ON and self.max_error > -DEADBAND: + self._setpoint = self.minimum_setpoint + _LOGGER.debug("Setting setpoint to minimum: %.1f°C", self.minimum_setpoint) + else: + self._setpoint = MINIMUM_SETPOINT + _LOGGER.debug("Setting setpoint to absolute minimum: %.1f°C", MINIMUM_SETPOINT) + # Apply the setpoint using the coordinator await self._coordinator.async_set_control_setpoint(self._setpoint) + _LOGGER.info("Control setpoint has been updated to: %.1f°C", self._setpoint) async def _async_control_relative_modulation(self) -> None: """Control the relative modulation value based on the conditions""" From f81bd58002a716cda69db650b92c11e773a393af Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 15:00:56 +0100 Subject: [PATCH 119/213] Add some more logging --- custom_components/sat/pwm.py | 83 +++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index 575a0c23..0336ebc3 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -41,6 +41,11 @@ def __init__(self, heating_curve: HeatingCurve, max_cycle_time: int, automatic_d self._min_duty_cycle_percentage = self._duty_cycle_lower_threshold / 2 self._max_duty_cycle_percentage = 1 - self._min_duty_cycle_percentage + _LOGGER.debug( + "Initialized PWM control with duty cycle thresholds - Lower: %.2f%%, Upper: %.2f%%", + self._duty_cycle_lower_threshold * 100, self._duty_cycle_upper_threshold * 100 + ) + self.reset() def reset(self) -> None: @@ -53,6 +58,8 @@ def reset(self) -> None: self._first_duty_cycle_start = None self._last_duty_cycle_percentage = None + _LOGGER.info("PWM control reset to initial state.") + async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: """Update the PWM state based on the output of a PID controller.""" if not self._heating_curve.value or requested_setpoint is None: @@ -60,52 +67,64 @@ async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: self._last_update = monotonic() self._last_boiler_temperature = boiler.temperature - reason = "heating curve value" if not self._heating_curve.value else "requested setpoint" - _LOGGER.warning(f"Turned off PWM due to lack of valid {reason}.") + reason = "missing heating curve value" if not self._heating_curve.value else "missing requested setpoint" + _LOGGER.warning("PWM turned off due to %s.", reason) return if self._last_boiler_temperature is None: self._last_boiler_temperature = boiler.temperature + _LOGGER.debug("Initialized last boiler temperature to %.1f°C", boiler.temperature) if self._first_duty_cycle_start and (monotonic() - self._first_duty_cycle_start) > 3600: self._cycles = 0 self._first_duty_cycle_start = None - _LOGGER.debug("Resetting CYCLES to zero, since an hour has passed.") + _LOGGER.info("CYCLES count reset after an hour.") elapsed = monotonic() - self._last_update self._duty_cycle = self._calculate_duty_cycle(requested_setpoint, boiler) - _LOGGER.debug("Calculated duty cycle %.0f seconds ON, %.0f seconds OFF, %d CYCLES this hour.", self._duty_cycle[0], self._duty_cycle[1], self._cycles) + _LOGGER.debug( + "Duty cycle calculated: %.0f seconds ON, %.0f seconds OFF, CYCLES this hour: %d", + self._duty_cycle[0], self._duty_cycle[1], self._cycles + ) + # Update boiler temperature if heater has just started up if self._state == PWMState.ON: if elapsed <= HEATER_STARTUP_TIMEFRAME: - self._last_boiler_temperature = self._alpha * boiler.temperature + (1 - self._alpha) * self._last_boiler_temperature + self._last_boiler_temperature = ( + self._alpha * boiler.temperature + (1 - self._alpha) * self._last_boiler_temperature + ) + + _LOGGER.debug("Updated last boiler temperature with weighted average during startup phase.") else: self._last_boiler_temperature = boiler.temperature + _LOGGER.debug("Updated last boiler temperature to %.1f°C", boiler.temperature) # State transitions for PWM - if self._state != PWMState.ON and self._duty_cycle[0] >= HEATER_STARTUP_TIMEFRAME and (elapsed >= self._duty_cycle[1] or self._state == PWMState.IDLE): + if self._state != PWMState.ON and self._duty_cycle[0] >= HEATER_STARTUP_TIMEFRAME and ( + elapsed >= self._duty_cycle[1] or self._state == PWMState.IDLE): if self._first_duty_cycle_start is None: self._first_duty_cycle_start = monotonic() if self._cycles >= self._max_cycles: - _LOGGER.debug("Preventing duty cycle due to max cycles per hour.") + _LOGGER.info("Reached max cycles per hour, preventing new duty cycle.") return self._cycles += 1 self._state = PWMState.ON self._last_update = monotonic() self._last_boiler_temperature = boiler.temperature - _LOGGER.debug("Starting duty cycle.") + _LOGGER.info("Starting new duty cycle (ON state). Current CYCLES count: %d", self._cycles) return - if self._state != PWMState.OFF and (self._duty_cycle[0] < HEATER_STARTUP_TIMEFRAME or elapsed >= self._duty_cycle[0] or self._state == PWMState.IDLE): + if self._state != PWMState.OFF and ( + self._duty_cycle[0] < HEATER_STARTUP_TIMEFRAME or elapsed >= self._duty_cycle[0] or self._state == PWMState.IDLE): self._state = PWMState.OFF self._last_update = monotonic() - _LOGGER.debug("Finished duty cycle.") + _LOGGER.info("Duty cycle completed. Switching to OFF state.") return - _LOGGER.debug("Cycle time elapsed %.0f seconds in %s", elapsed, self._state) + _LOGGER.debug("Cycle time elapsed: %.0f seconds in state: %s", elapsed, self._state) def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) -> Tuple[int, int]: """Calculate the duty cycle in seconds based on the output of a PID controller and a heating curve value.""" @@ -117,21 +136,23 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) # Calculate duty cycle percentage self._last_duty_cycle_percentage = (requested_setpoint - base_offset) / (boiler_temperature - base_offset) - self._last_duty_cycle_percentage = min(self._last_duty_cycle_percentage, 1) - self._last_duty_cycle_percentage = max(self._last_duty_cycle_percentage, 0) + self._last_duty_cycle_percentage = min(max(self._last_duty_cycle_percentage, 0), 1) - _LOGGER.debug("Requested setpoint %.1f", requested_setpoint) - _LOGGER.debug("Boiler Temperature %.1f", boiler_temperature) - - _LOGGER.debug("Calculated duty cycle %.2f%%", self._last_duty_cycle_percentage * 100) - _LOGGER.debug("Calculated duty cycle lower threshold %.2f%%", self._duty_cycle_lower_threshold * 100) - _LOGGER.debug("Calculated duty cycle upper threshold %.2f%%", self._duty_cycle_upper_threshold * 100) + _LOGGER.debug( + "Duty cycle calculation - Requested setpoint: %.1f°C, Boiler temperature: %.1f°C, Duty cycle percentage: %.2f%%", + requested_setpoint, boiler_temperature, self._last_duty_cycle_percentage * 100 + ) # If automatic duty cycle control is disabled if not self._automatic_duty_cycle: on_time = self._last_duty_cycle_percentage * self._max_cycle_time off_time = (1 - self._last_duty_cycle_percentage) * self._max_cycle_time + _LOGGER.debug( + "Calculated on_time: %.0f seconds, off_time: %.0f seconds.", + on_time, off_time + ) + return int(on_time), int(off_time) # Handle special low-duty cycle cases @@ -140,33 +161,55 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) on_time = self._on_time_lower_threshold off_time = self._on_time_max_threshold - self._on_time_lower_threshold + _LOGGER.debug( + "Special low-duty case with flame active. Setting on_time: %d seconds, off_time: %d seconds.", + on_time, off_time + ) + return int(on_time), int(off_time) + _LOGGER.debug("Special low-duty case without flame. Setting on_time: 0 seconds, off_time: %d seconds.", self._on_time_max_threshold) return 0, int(self._on_time_max_threshold) - # Map duty cycle ranges to on/off times + # Mapping duty cycle ranges to on/off times if self._last_duty_cycle_percentage <= self._duty_cycle_lower_threshold: on_time = self._on_time_lower_threshold off_time = (self._on_time_lower_threshold / self._last_duty_cycle_percentage) - self._on_time_lower_threshold + _LOGGER.debug( + "Low duty cycle range. Calculated on_time: %d seconds, off_time: %d seconds.", + on_time, off_time + ) + return int(on_time), int(off_time) if self._last_duty_cycle_percentage <= self._duty_cycle_upper_threshold: on_time = self._on_time_upper_threshold * self._last_duty_cycle_percentage off_time = self._on_time_upper_threshold * (1 - self._last_duty_cycle_percentage) + _LOGGER.debug( + "Mid-range duty cycle. Calculated on_time: %d seconds, off_time: %d seconds.", + on_time, off_time + ) + return int(on_time), int(off_time) if self._last_duty_cycle_percentage <= self._max_duty_cycle_percentage: on_time = self._on_time_lower_threshold / (1 - self._last_duty_cycle_percentage) - self._on_time_lower_threshold off_time = self._on_time_lower_threshold + _LOGGER.debug( + "High duty cycle range. Calculated on_time: %d seconds, off_time: %d seconds.", + on_time, off_time + ) + return int(on_time), int(off_time) # Handle cases where the duty cycle exceeds the maximum allowed percentage on_time = self._on_time_max_threshold off_time = 0 + _LOGGER.debug("Maximum duty cycle exceeded. Setting on_time: %d seconds, off_time: %d seconds.", on_time, off_time) return int(on_time), int(off_time) @property From 4da17045cdca159699cd27c98f9ac9175853a2ec Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 15:04:02 +0100 Subject: [PATCH 120/213] Make sure we have all the required values before proceeding --- custom_components/sat/pwm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index 0336ebc3..ed733fbd 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -62,13 +62,13 @@ def reset(self) -> None: async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: """Update the PWM state based on the output of a PID controller.""" - if not self._heating_curve.value or requested_setpoint is None: + if not self._heating_curve.value or requested_setpoint is None or boiler.temperature is None: self._state = PWMState.IDLE self._last_update = monotonic() self._last_boiler_temperature = boiler.temperature - reason = "missing heating curve value" if not self._heating_curve.value else "missing requested setpoint" - _LOGGER.warning("PWM turned off due to %s.", reason) + _LOGGER.warning("PWM turned off due missing values.") + return if self._last_boiler_temperature is None: @@ -92,7 +92,7 @@ async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: if self._state == PWMState.ON: if elapsed <= HEATER_STARTUP_TIMEFRAME: self._last_boiler_temperature = ( - self._alpha * boiler.temperature + (1 - self._alpha) * self._last_boiler_temperature + self._alpha * boiler.temperature + (1 - self._alpha) * self._last_boiler_temperature ) _LOGGER.debug("Updated last boiler temperature with weighted average during startup phase.") From e333813c0e211e4ebbb72105531f0cf31a8f4f3a Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 15:10:22 +0100 Subject: [PATCH 121/213] Remove duplicate logging --- custom_components/sat/pwm.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index ed733fbd..64d74511 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -83,11 +83,6 @@ async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: elapsed = monotonic() - self._last_update self._duty_cycle = self._calculate_duty_cycle(requested_setpoint, boiler) - _LOGGER.debug( - "Duty cycle calculated: %.0f seconds ON, %.0f seconds OFF, CYCLES this hour: %d", - self._duty_cycle[0], self._duty_cycle[1], self._cycles - ) - # Update boiler temperature if heater has just started up if self._state == PWMState.ON: if elapsed <= HEATER_STARTUP_TIMEFRAME: @@ -177,8 +172,8 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) off_time = (self._on_time_lower_threshold / self._last_duty_cycle_percentage) - self._on_time_lower_threshold _LOGGER.debug( - "Low duty cycle range. Calculated on_time: %d seconds, off_time: %d seconds.", - on_time, off_time + "Low duty cycle range, cycles this hour: %d Calculated on_time: %d seconds, off_time: %d seconds.", + self._cycles, on_time, off_time ) return int(on_time), int(off_time) @@ -188,8 +183,8 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) off_time = self._on_time_upper_threshold * (1 - self._last_duty_cycle_percentage) _LOGGER.debug( - "Mid-range duty cycle. Calculated on_time: %d seconds, off_time: %d seconds.", - on_time, off_time + "Mid-range duty cycle, cycles this hour: %d. %d Calculated on_time: %d seconds, off_time: %d seconds.", + self._cycles, on_time, off_time ) return int(on_time), int(off_time) @@ -199,8 +194,8 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) off_time = self._on_time_lower_threshold _LOGGER.debug( - "High duty cycle range. Calculated on_time: %d seconds, off_time: %d seconds.", - on_time, off_time + "High duty cycle range, cycles this hour: %d. Calculated on_time: %d seconds, off_time: %d seconds.", + self._cycles, on_time, off_time ) return int(on_time), int(off_time) From 4748ee782fa854f26d5547d45a22e009b4cf1117 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 15:11:40 +0100 Subject: [PATCH 122/213] Typo --- custom_components/sat/pwm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index 64d74511..51b7a70e 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -172,7 +172,7 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) off_time = (self._on_time_lower_threshold / self._last_duty_cycle_percentage) - self._on_time_lower_threshold _LOGGER.debug( - "Low duty cycle range, cycles this hour: %d Calculated on_time: %d seconds, off_time: %d seconds.", + "Low duty cycle range, cycles this hour: %d. Calculated on_time: %d seconds, off_time: %d seconds.", self._cycles, on_time, off_time ) From f9a12f8df9d7f548378329acbc8682f7620238be Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 30 Nov 2024 15:29:44 +0100 Subject: [PATCH 123/213] Typo --- custom_components/sat/pwm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index 51b7a70e..2494806e 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -183,7 +183,7 @@ def _calculate_duty_cycle(self, requested_setpoint: float, boiler: BoilerState) off_time = self._on_time_upper_threshold * (1 - self._last_duty_cycle_percentage) _LOGGER.debug( - "Mid-range duty cycle, cycles this hour: %d. %d Calculated on_time: %d seconds, off_time: %d seconds.", + "Mid-range duty cycle, cycles this hour: %d. Calculated on_time: %d seconds, off_time: %d seconds.", self._cycles, on_time, off_time ) From 6a2e733d9e42634c4016224930cf125dc4dd177d Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Thu, 5 Dec 2024 23:00:04 +0100 Subject: [PATCH 124/213] Cleaning up, add support for an Area manager, add EMS-ESP support and migrate to a universal MQTT Coordinator --- .pylintrc | 19 ++ custom_components/sat/__init__.py | 107 +++---- custom_components/sat/area.py | 137 +++++++++ custom_components/sat/climate.py | 72 ++--- custom_components/sat/config_flow.py | 33 ++- custom_components/sat/const.py | 3 +- custom_components/sat/coordinator.py | 12 +- custom_components/sat/manifest.json | 2 +- custom_components/sat/manufacturer.py | 18 +- .../sat/manufacturers/dedietrich.py | 2 +- .../sat/manufacturers/ferroli.py | 2 +- .../sat/manufacturers/geminox.py | 2 +- custom_components/sat/manufacturers/ideal.py | 2 +- .../sat/manufacturers/immergas.py | 2 +- .../sat/manufacturers/intergas.py | 2 +- custom_components/sat/manufacturers/nefit.py | 2 +- .../sat/manufacturers/simulator.py | 2 +- custom_components/sat/mqtt/__init__.py | 279 +++++------------- custom_components/sat/mqtt/ems.py | 165 +++++++++++ custom_components/sat/mqtt/opentherm.py | 198 +++++++++++++ custom_components/sat/overshoot_protection.py | 4 +- custom_components/sat/relative_modulation.py | 6 +- custom_components/sat/serial/__init__.py | 5 + custom_components/sat/util.py | 16 +- 24 files changed, 744 insertions(+), 348 deletions(-) create mode 100644 .pylintrc create mode 100644 custom_components/sat/area.py create mode 100644 custom_components/sat/mqtt/ems.py create mode 100644 custom_components/sat/mqtt/opentherm.py diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..ee63ef26 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,19 @@ +[MASTER] +# Specify a configuration file. +rcfile= + +[REPORTS] +# Set the output format. Available formats: text, parseable, colorized, msvs (visual studio). +output-format=text + +[FORMAT] +# Maximum number of characters on a single line. +max-line-length=200 + +[DESIGN] +# Maximum number of arguments for function / method. +max-args=5 + +[TYPECHECK] +# Tells whether missing members accessed in mixin class should be ignored. +ignore-mixin-members=yes diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index aaf7cea9..8c5e91d6 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -7,9 +7,9 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry from homeassistant.helpers.storage import Store -from . import mqtt, serial, switch from .const import * from .coordinator import SatDataUpdateCoordinatorFactory @@ -17,144 +17,151 @@ PLATFORMS = [CLIMATE_DOMAIN, SENSOR_DOMAIN, NUMBER_DOMAIN, BINARY_SENSOR_DOMAIN] -async def async_setup_entry(_hass: HomeAssistant, _entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """ Set up this integration using the UI. This function is called by Home Assistant when the integration is set up with the UI. """ # Make sure we have our default domain property - _hass.data.setdefault(DOMAIN, {}) + hass.data.setdefault(DOMAIN, {}) # Create a new dictionary for this entry - _hass.data[DOMAIN][_entry.entry_id] = {} + hass.data[DOMAIN][entry.entry_id] = {} # Resolve the coordinator by using the factory according to the mode - _hass.data[DOMAIN][_entry.entry_id][COORDINATOR] = await SatDataUpdateCoordinatorFactory().resolve( - hass=_hass, data=_entry.data, options=_entry.options, mode=_entry.data.get(CONF_MODE), device=_entry.data.get(CONF_DEVICE) + hass.data[DOMAIN][entry.entry_id][COORDINATOR] = await SatDataUpdateCoordinatorFactory().resolve( + hass=hass, data=entry.data, options=entry.options, mode=entry.data.get(CONF_MODE), device=entry.data.get(CONF_DEVICE) ) # Forward entry setup for used platforms - await _entry.async_create_task(_hass, _hass.config_entries.async_forward_entry_setups(_entry, PLATFORMS)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Add an update listener for this entry - _entry.async_on_unload(_entry.add_update_listener(async_reload_entry)) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True -async def async_unload_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """ Handle removal of an entry. This function is called by Home Assistant when the integration is being removed. """ - climate = _hass.data[DOMAIN][_entry.entry_id][CLIMATE] - await _hass.data[DOMAIN][_entry.entry_id][COORDINATOR].async_will_remove_from_hass(climate) + climate = hass.data[DOMAIN][entry.entry_id][CLIMATE] + await hass.data[DOMAIN][entry.entry_id][COORDINATOR].async_will_remove_from_hass(climate) unloaded = all( # Forward entry unload for used platforms - await asyncio.gather(_hass.config_entries.async_unload_platforms(_entry, PLATFORMS)) + await asyncio.gather(hass.config_entries.async_unload_platforms(entry, PLATFORMS)) ) # Remove the entry from the data dictionary if all components are unloaded successfully if unloaded: - _hass.data[DOMAIN].pop(_entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unloaded -async def async_reload_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> None: +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """ Reload config entry. This function is called by Home Assistant when the integration configuration is updated. """ # Unload the entry and its dependent components - await async_unload_entry(_hass, _entry) + await async_unload_entry(hass, entry) # Set up the entry again - await async_setup_entry(_hass, _entry) + await async_setup_entry(hass, entry) -async def async_migrate_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool: +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate old entry.""" - from custom_components.sat.config_flow import SatFlowHandler - _LOGGER.debug("Migrating from version %s", _entry.version) + from .config_flow import SatFlowHandler + _LOGGER.debug("Migrating from version %s", entry.version) - if _entry.version < SatFlowHandler.VERSION: - new_data = {**_entry.data} - new_options = {**_entry.options} + if entry.version < SatFlowHandler.VERSION: + new_data = {**entry.data} + new_options = {**entry.options} - if _entry.version < 2: - if not _entry.data.get("minimum_setpoint"): + if entry.version < 2: + if not entry.data.get("minimum_setpoint"): # Legacy Store - store = Store(_hass, 1, DOMAIN) + store = Store(hass, 1, DOMAIN) new_data["minimum_setpoint"] = 10 if (data := await store.async_load()) and (overshoot_protection_value := data.get("overshoot_protection_value")): new_data["minimum_setpoint"] = overshoot_protection_value - if _entry.options.get("heating_system") == "underfloor": + if entry.options.get("heating_system") == "underfloor": new_data["heating_system"] = "underfloor" else: new_data["heating_system"] = "radiators" - if not _entry.data.get("maximum_setpoint"): + if not entry.data.get("maximum_setpoint"): new_data["maximum_setpoint"] = 55 - if _entry.options.get("heating_system") == "underfloor": + if entry.options.get("heating_system") == "underfloor": new_data["maximum_setpoint"] = 50 - if _entry.options.get("heating_system") == "radiator_low_temperatures": + if entry.options.get("heating_system") == "radiator_low_temperatures": new_data["maximum_setpoint"] = 55 - if _entry.options.get("heating_system") == "radiator_medium_temperatures": + if entry.options.get("heating_system") == "radiator_medium_temperatures": new_data["maximum_setpoint"] = 65 - if _entry.options.get("heating_system") == "radiator_high_temperatures": + if entry.options.get("heating_system") == "radiator_high_temperatures": new_data["maximum_setpoint"] = 75 - if _entry.version < 3: - if main_climates := _entry.options.get("main_climates"): - new_data[CONF_MAIN_CLIMATES] = main_climates + if entry.version < 3: + if main_climates := entry.options.get("main_climates"): + new_data["main_climates"] = main_climates new_options.pop("main_climates") - if secondary_climates := _entry.options.get("climates"): + if secondary_climates := entry.options.get("climates"): new_data["secondary_climates"] = secondary_climates new_options.pop("climates") - if sync_with_thermostat := _entry.options.get("sync_with_thermostat"): + if sync_with_thermostat := entry.options.get("sync_with_thermostat"): new_data["sync_with_thermostat"] = sync_with_thermostat new_options.pop("sync_with_thermostat") - if _entry.version < 4: - if _entry.data.get("window_sensor") is not None: - new_data["window_sensors"] = [_entry.data.get("window_sensor")] + if entry.version < 4: + if entry.data.get("window_sensor") is not None: + new_data["window_sensors"] = [entry.data.get("window_sensor")] del new_options["window_sensor"] - if _entry.version < 5: - if _entry.options.get("overshoot_protection") is not None: - new_data[CONF_OVERSHOOT_PROTECTION] = _entry.options.get("overshoot_protection") + if entry.version < 5: + if entry.options.get("overshoot_protection") is not None: + new_data["overshoot_protection"] = entry.options.get("overshoot_protection") del new_options["overshoot_protection"] - if _entry.version < 7: + if entry.version < 7: new_options["pid_controller_version"] = 1 - if _entry.version < 8: - if _entry.options.get("heating_curve_version") is not None and int(_entry.options.get("heating_curve_version")) < 2: + if entry.version < 8: + if entry.options.get("heating_curve_version") is not None and int(entry.options.get("heating_curve_version")) < 2: new_options["heating_curve_version"] = 3 - if _entry.version < 9: - if _entry.data.get("heating_system") == "heat_pump": + if entry.version < 9: + if entry.data.get("heating_system") == "heat_pump": new_options["cycles_per_hour"] = 2 - if _entry.data.get("heating_system") == "radiators": + if entry.data.get("heating_system") == "radiators": new_options["cycles_per_hour"] = 3 - _hass.config_entries.async_update_entry(_entry, version=SatFlowHandler.VERSION, data=new_data, options=new_options) + if entry.version < 10: + if entry.data.get("mode") == "mqtt": + device = device_registry.async_get(hass).async_get(entry.data.get("device")) - _LOGGER.info("Migration to version %s successful", _entry.version) + new_data["mode"] = "mqtt_opentherm" + new_data["device"] = list(device.identifiers)[0][1] + + hass.config_entries.async_update_entry(entry, version=SatFlowHandler.VERSION, data=new_data, options=new_options) + + _LOGGER.info("Migration to version %s successful", entry.version) return True diff --git a/custom_components/sat/area.py b/custom_components/sat/area.py new file mode 100644 index 00000000..1db79f0a --- /dev/null +++ b/custom_components/sat/area.py @@ -0,0 +1,137 @@ +from types import MappingProxyType +from typing import Any, List + +from homeassistant.components.climate import HVACMode +from homeassistant.const import STATE_UNKNOWN, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant, State + +from .util import ( + create_pwm_controller, + create_pid_controller, + create_heating_curve_controller, float_value, +) + +SENSOR_TEMPERATURE_ID = "sensor_temperature_id" + + +class Area: + def __init__(self, config_data: MappingProxyType[str, Any], config_options: MappingProxyType[str, Any], entity_id: str): + self._hass = None + self._entity_id = entity_id + + # Create controllers with the given configuration options + self.pid = create_pid_controller(config_options) + self.heating_curve = create_heating_curve_controller(config_data, config_options) + self.pwm = create_pwm_controller(self.heating_curve, config_data, config_options) + + @property + def id(self) -> str: + return self._entity_id + + @property + def state(self) -> State | None: + """Retrieve the current state of the climate entity.""" + if (self._hass is None) or (state := self._hass.states.get(self._entity_id)) is None: + return None + + return state if state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE] else None + + @property + def target_temperature(self) -> float | None: + """Retrieve the target temperature from the climate entity.""" + if (self._hass is None) or (state := self.state) is None: + return None + + return float_value(state.attributes.get("temperature")) + + @property + def current_temperature(self) -> float | None: + """Retrieve the current temperature, overridden by a sensor if set.""" + if (self._hass is None) or (state := self.state) is None: + return None + + # Check if there is an overridden sensor temperature + if sensor_temperature_id := state.attributes.get(SENSOR_TEMPERATURE_ID): + sensor_state = self._hass.states.get(sensor_temperature_id) + if sensor_state and sensor_state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE, HVACMode.OFF]: + return float_value(sensor_state.state) + + return float_value(state.attributes.get("current_temperature") or self.target_temperature) + + @property + def error(self) -> float | None: + """Calculate the temperature error.""" + target_temperature = self.target_temperature + current_temperature = self.current_temperature + + if target_temperature is None or current_temperature is None: + return None + + return round(target_temperature - current_temperature, 2) + + async def async_added_to_hass(self, hass: HomeAssistant): + self._hass = hass + + async def async_control_heating_loop(self, _time=None) -> None: + """Asynchronously control the heating loop.""" + if (temperature_error := self.error) is not None: + # Control the integral (if exceeded the time limit) + self.pid.update_integral(temperature_error, self.heating_curve.value) + + +class Areas: + def __init__(self, config_data: MappingProxyType[str, Any], config_options: MappingProxyType[str, Any], entity_ids: list): + """Initialize Areas with multiple Area instances using shared config data and options.""" + self._entity_ids = entity_ids + self._config_data = config_data + self._config_options = config_options + self._areas = [Area(config_data, config_options, entity_id) for entity_id in entity_ids] + + @property + def errors(self) -> List[float]: + """Return a list of all the error values for all areas.""" + return [area.error for area in self._areas if area.error is not None] + + @property + def heating_curves(self): + """Return an interface to update heating curves for all areas.""" + return Areas._HeatingCurves(self._areas) + + @property + def pids(self): + """Return an interface to reset PID controllers for all areas.""" + return Areas._PIDs(self._areas) + + async def async_added_to_hass(self, hass: HomeAssistant): + for area in self._areas: + await area.async_added_to_hass(hass) + + async def async_control_heating_loops(self, _time=None) -> None: + """Asynchronously control heating loop for all areas.""" + for area in self._areas: + await area.async_control_heating_loop(_time) + + class _HeatingCurves: + def __init__(self, areas: list[Area]): + self.areas = areas + + def update(self, current_outside_temperature: float) -> None: + """Update the heating curve for all areas based on current outside temperature.""" + for area in self.areas: + if area.target_temperature is None: + continue + + area.heating_curve.update(area.target_temperature, current_outside_temperature) + + class _PIDs: + def __init__(self, areas: list[Area]): + self.areas = areas + + def update(self, boiler_temperature: float) -> None: + for area in self.areas: + area.pid.update(area.error, area.heating_curve.value, boiler_temperature) + + def reset(self) -> None: + """Reset PID controllers for all areas.""" + for area in self.areas: + area.pid.reset() diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 41f428cf..82e89def 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -5,7 +5,6 @@ import logging from datetime import timedelta from time import monotonic, time -from typing import List from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import ( @@ -36,6 +35,7 @@ from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity +from .area import Areas, SENSOR_TEMPERATURE_ID from .boiler_state import BoilerState from .const import * from .coordinator import SatDataUpdateCoordinator, DeviceState @@ -55,8 +55,6 @@ ATTR_PRE_ACTIVITY_TEMPERATURE = "pre_activity_temperature" ATTR_ADJUSTED_MINIMUM_SETPOINTS = "adjusted_minimum_setpoints" -SENSOR_TEMPERATURE_ID = "sensor_temperature_id" - _LOGGER = logging.getLogger(__name__) @@ -190,6 +188,9 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn # Create Relative Modulation controller self._relative_modulation = RelativeModulation(coordinator, self._heating_system) + # Create Area controllers + self._areas = Areas(config_entry.data, config_options, self._climates) + if self._simulation: _LOGGER.warning("Simulation mode!") @@ -205,6 +206,7 @@ async def async_added_to_hass(self) -> None: # Update a heating curve if outside temperature is available if self.current_outside_temperature is not None: + self._areas.heating_curves.update(self.current_outside_temperature) self.heating_curve.update(self.target_temperature, self.current_outside_temperature) # Start control loop @@ -216,6 +218,9 @@ async def async_added_to_hass(self) -> None: # Initialize minimum setpoint system await self._minimum_setpoint.async_initialize(self.hass) + # Initialize the area system + await self._areas.async_added_to_hass(self.hass) + # Let the coordinator know we are ready await self._coordinator.async_added_to_hass(self) @@ -335,6 +340,7 @@ async def _register_services(self): async def reset_integral(_call: ServiceCall): """Service to reset the integral part of the PID controller.""" self.pid.reset() + self._areas.pids.reset() self.hass.services.async_register(DOMAIN, SERVICE_RESET_INTEGRAL, reset_integral) @@ -466,7 +472,7 @@ def max_error(self) -> float: if self._heating_mode == HEATING_MODE_ECO: return self.error - return max([self.error] + self.climate_errors) + return max([self.error] + self._areas.errors) @property def setpoint(self) -> float | None: @@ -480,34 +486,6 @@ def requested_setpoint(self) -> float: return round(max(self.heating_curve.value + self.pid.output, MINIMUM_SETPOINT), 1) - @property - def climate_errors(self) -> List[float]: - """Calculate the temperature difference between the current temperature and target temperature for all connected climates.""" - errors = [] - for climate in self._climates: - # Skip if climate state is unavailable or HVAC mode is off - state = self.hass.states.get(climate) - if state is None or state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE, HVACMode.OFF]: - continue - - # Calculate temperature difference for this climate - target_temperature = float(state.attributes.get("temperature")) - current_temperature = float(state.attributes.get("current_temperature") or target_temperature) - - # Retrieve the overridden sensor temperature if set - if sensor_temperature_id := state.attributes.get(SENSOR_TEMPERATURE_ID): - sensor_state = self.hass.states.get(sensor_temperature_id) - if sensor_state is not None and sensor_state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE, HVACMode.OFF]: - current_temperature = float(sensor_state.state) - - # Calculate the error value - error = round(target_temperature - current_temperature, 2) - - # Add to the list, so we calculate the max. later - errors.append(error) - - return errors - @property def valves_open(self) -> bool: """Determine if any of the controlled thermostats have open valves.""" @@ -675,9 +653,8 @@ async def _async_climate_changed(self, event: Event) -> None: await self._async_control_pid(True) # If the current temperature has changed, update the PID controller - elif not hasattr(new_state.attributes, SENSOR_TEMPERATURE_ID) and new_attrs.get("current_temperature") != old_attrs.get( - "current_temperature"): - await self._async_control_pid(False) + elif not hasattr(new_state.attributes, SENSOR_TEMPERATURE_ID) and new_attrs.get("current_temperature") != old_attrs.get("current_temperature"): + await self._async_control_pid() if (self._rooms is not None and new_state.entity_id not in self._rooms) or self.preset_mode in [PRESET_HOME, PRESET_COMFORT]: if target_temperature := new_state.attributes.get("temperature"): @@ -696,7 +673,7 @@ async def _async_temperature_change(self, event: Event) -> None: return _LOGGER.debug(f"Climate Sensor Changed ({new_state.entity_id}).") - await self._async_control_pid(False) + await self._async_control_pid() await self.async_control_heating_loop() async def _async_window_sensor_changed(self, event: Event) -> None: @@ -742,15 +719,14 @@ async def _async_control_pid(self, reset: bool = False) -> None: # Reset the PID controller if the sensor data is too old if self._sensor_max_value_age != 0 and monotonic() - self.pid.last_updated > self._sensor_max_value_age: self.pid.reset() + self._areas.pids.reset() # Calculate the maximum error between the current temperature and the target temperature of all climates max_error = self.max_error # Make sure we use the latest heating curve value - self.heating_curve.update( - target_temperature=self.target_temperature, - outside_temperature=self.current_outside_temperature, - ) + self.heating_curve.update(self.target_temperature, self.current_outside_temperature) + self._areas.heating_curves.update(self.current_outside_temperature) # Update the PID controller with the maximum error if not reset: @@ -776,11 +752,8 @@ async def _async_control_pid(self, reset: bool = False) -> None: _LOGGER.info("Reached deadband, turning off warming up.") self._warming_up_data = None - self.pid.update( - error=max_error, - heating_curve_value=self.heating_curve.value, - boiler_temperature=self._coordinator.filtered_boiler_temperature - ) + self._areas.pids.update(self._coordinator.filtered_boiler_temperature) + self.pid.update(max_error, self.heating_curve.value, self._coordinator.filtered_boiler_temperature) elif max_error != self.pid.last_error: _LOGGER.info(f"Updating error value to {max_error} (Reset: True)") @@ -913,6 +886,10 @@ async def async_control_heating_loop(self, _time=None) -> None: # Control the integral (if exceeded the time limit) self.pid.update_integral(self.max_error, self.heating_curve.value) + # Control our areas + await self._areas.async_control_heating_loops() + + # Control our dynamic minimum setpoint if not self._coordinator.hot_water_active and self._coordinator.flame_active: # Calculate the base return temperature if self.warming_up: @@ -922,10 +899,7 @@ async def async_control_heating_loop(self, _time=None) -> None: self._minimum_setpoint.calculate(self._coordinator.return_temperature) # If the setpoint is high and the HVAC is not off, turn on the heater - if self._setpoint > MINIMUM_SETPOINT: - await self.async_set_heater_state(DeviceState.ON) - else: - await self.async_set_heater_state(DeviceState.OFF) + await self.async_set_heater_state(DeviceState.ON if self._setpoint > MINIMUM_SETPOINT else DeviceState.OFF) self.async_write_ha_state() diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 1c22d5c5..f7cb1a24 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -36,7 +36,7 @@ class SatFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for SAT.""" - VERSION = 9 + VERSION = 10 MINOR_VERSION = 0 calibration = None @@ -84,23 +84,34 @@ async def async_step_dhcp(self, discovery_info: DhcpServiceInfo): # abort if we already have exactly this gateway id/host # reload the integration if the host got updated await self.async_set_unique_id(discovery_info.hostname) - self._abort_if_unique_id_configured(updates=self.data, reload_on_update=True) + self._abort_if_unique_id_configured(updates=self.data) return await self.async_step_serial() async def async_step_mqtt(self, discovery_info: MqttServiceInfo): """Handle dhcp discovery.""" + device_id = "unknown" + device_name = "unknown" + + if discovery_info.topic[:5] == "OTGW/": + device_id = discovery_info.topic[11:] + device_name = "OTGW" + + if discovery_info.topic[:8] == "ems-esp/": + device_id = "ems-esp" + device_name = "EMS-ESP" + device = device_registry.async_get(self.hass).async_get_device( - {(MQTT_DOMAIN, discovery_info.topic[11:])} + {(MQTT_DOMAIN, device_id)} ) - _LOGGER.debug("Discovered OTGW at [mqtt://%s]", discovery_info.topic) - self.data[CONF_DEVICE] = device.id + _LOGGER.debug("Discovered %s at [mqtt://%s]", device_name, discovery_info.topic) + self.data[CONF_DEVICE] = device_id # abort if we already have exactly this gateway id/host # reload the integration if the host got updated await self.async_set_unique_id(device.id) - self._abort_if_unique_id_configured(updates=self.data, reload_on_update=True) + self._abort_if_unique_id_configured(updates=self.data) return await self.async_step_mosquitto() @@ -109,7 +120,7 @@ async def async_step_mosquitto(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: self.data.update(_user_input) - self.data[CONF_MODE] = MODE_MQTT + self.data[CONF_MODE] = MODE_MQTT_OPENTHERM if not await mqtt.async_wait_for_mqtt_client(self.hass): self.errors["base"] = "mqtt_component" @@ -124,9 +135,7 @@ async def async_step_mosquitto(self, _user_input: dict[str, Any] | None = None): data_schema=vol.Schema({ vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_MQTT_TOPIC, default=OPTIONS_DEFAULTS[CONF_MQTT_TOPIC]): str, - vol.Required(CONF_DEVICE, default=self.data.get(CONF_DEVICE)): selector.DeviceSelector( - selector.DeviceSelectorConfig(model="otgw-nodo") - ), + vol.Required(CONF_DEVICE, default=self.data.get(CONF_DEVICE, "otgw-XXXXXXXXXXXX")): str, }), ) @@ -239,7 +248,7 @@ async def async_step_sensors(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: self.data.update(_user_input) - if self.data[CONF_MODE] in [MODE_ESPHOME, MODE_MQTT, MODE_SERIAL, MODE_SIMULATOR]: + if self.data[CONF_MODE] in [MODE_ESPHOME, MODE_MQTT_OPENTHERM, MODE_SERIAL, MODE_SIMULATOR]: return await self.async_step_heating_system() return await self.async_step_areas() @@ -628,7 +637,7 @@ async def async_step_advanced(self, _user_input: dict[str, Any] | None = None): vol.Required(CONF_DYNAMIC_MINIMUM_SETPOINT, default=options[CONF_DYNAMIC_MINIMUM_SETPOINT]): bool } - if options.get(CONF_MODE) in [MODE_MQTT, MODE_SERIAL, MODE_SIMULATOR]: + if options.get(CONF_MODE) in [MODE_MQTT_OPENTHERM, MODE_SERIAL, MODE_SIMULATOR]: schema[vol.Required(CONF_FORCE_PULSE_WIDTH_MODULATION, default=options[CONF_FORCE_PULSE_WIDTH_MODULATION])] = bool schema[vol.Required(CONF_MINIMUM_CONSUMPTION, default=options[CONF_MINIMUM_CONSUMPTION])] = selector.NumberSelector( diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index 384ebdeb..394368ba 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -7,7 +7,8 @@ CONFIG_STORE = "config_store" MODE_FAKE = "fake" -MODE_MQTT = "mqtt" +MODE_MQTT_EMS = "mqtt_ems" +MODE_MQTT_OPENTHERM = "mqtt_opentherm" MODE_SWITCH = "switch" MODE_SERIAL = "serial" MODE_ESPHOME = "esphome" diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index aea5c80d..a1b529f0 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -50,13 +50,17 @@ async def resolve( from .esphome import SatEspHomeCoordinator return SatEspHomeCoordinator(hass=hass, device_id=device, data=data, options=options) - if mode == MODE_MQTT: - from .mqtt import SatMqttCoordinator - return await SatMqttCoordinator(hass=hass, device_id=device, data=data, options=options).boot() + if mode == MODE_MQTT_EMS: + from .mqtt.ems import SatEmsMqttCoordinator + return SatEmsMqttCoordinator(hass=hass, device_id=device, data=data, options=options) + + if mode == MODE_MQTT_OPENTHERM: + from .mqtt.opentherm import SatOpenThermMqttCoordinator + return SatOpenThermMqttCoordinator(hass=hass, device_id=device, data=data, options=options) if mode == MODE_SERIAL: from .serial import SatSerialCoordinator - return await SatSerialCoordinator(hass=hass, port=device, data=data, options=options).async_connect() + return SatSerialCoordinator(hass=hass, port=device, data=data, options=options) raise Exception(f'Invalid mode[{mode}]') diff --git a/custom_components/sat/manifest.json b/custom_components/sat/manifest.json index 6b0939d1..c2c7cfa2 100644 --- a/custom_components/sat/manifest.json +++ b/custom_components/sat/manifest.json @@ -18,9 +18,9 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/Alexwijn/SAT/issues", "mqtt": [ + "ems-esp/+", "OTGW/value/+" ], - "requirements": [ "pyotgw==2.1.3" ], diff --git a/custom_components/sat/manufacturer.py b/custom_components/sat/manufacturer.py index e49bd449..077ddd9a 100644 --- a/custom_components/sat/manufacturer.py +++ b/custom_components/sat/manufacturer.py @@ -12,35 +12,35 @@ class ManufacturerFactory: @abstractmethod def resolve(self, member_id: int) -> Manufacturer | None: if member_id == -1: - from custom_components.sat.manufacturers.simulator import Simulator + from .manufacturers.simulator import Simulator return Simulator() - + if member_id == 4: - from custom_components.sat.manufacturers.geminox import Geminox + from .manufacturers.geminox import Geminox return Geminox() if member_id == 6: - from custom_components.sat.manufacturers.ideal import Ideal + from .manufacturers.ideal import Ideal return Ideal() if member_id == 9: - from custom_components.sat.manufacturers.ferroli import Ferroli + from .manufacturers.ferroli import Ferroli return Ferroli() if member_id == 11: - from custom_components.sat.manufacturers.dedietrich import DeDietrich + from .manufacturers.dedietrich import DeDietrich return DeDietrich() if member_id == 27: - from custom_components.sat.manufacturers.immergas import Immergas + from .manufacturers.immergas import Immergas return Immergas() if member_id == 131: - from custom_components.sat.manufacturers.nefit import Nefit + from .manufacturers.nefit import Nefit return Nefit() if member_id == 173: - from custom_components.sat.manufacturers.intergas import Intergas + from .manufacturers.intergas import Intergas return Intergas() return None diff --git a/custom_components/sat/manufacturers/dedietrich.py b/custom_components/sat/manufacturers/dedietrich.py index b3f1bda7..42e4b3b2 100644 --- a/custom_components/sat/manufacturers/dedietrich.py +++ b/custom_components/sat/manufacturers/dedietrich.py @@ -1,4 +1,4 @@ -from custom_components.sat.manufacturer import Manufacturer +from ..manufacturer import Manufacturer class DeDietrich(Manufacturer): diff --git a/custom_components/sat/manufacturers/ferroli.py b/custom_components/sat/manufacturers/ferroli.py index a08ae1fa..05ad5ce6 100644 --- a/custom_components/sat/manufacturers/ferroli.py +++ b/custom_components/sat/manufacturers/ferroli.py @@ -1,4 +1,4 @@ -from custom_components.sat.manufacturer import Manufacturer +from ..manufacturer import Manufacturer class Ferroli(Manufacturer): diff --git a/custom_components/sat/manufacturers/geminox.py b/custom_components/sat/manufacturers/geminox.py index 08be39ec..358e83a4 100644 --- a/custom_components/sat/manufacturers/geminox.py +++ b/custom_components/sat/manufacturers/geminox.py @@ -1,4 +1,4 @@ -from custom_components.sat.manufacturer import Manufacturer +from ..manufacturer import Manufacturer class Geminox(Manufacturer): diff --git a/custom_components/sat/manufacturers/ideal.py b/custom_components/sat/manufacturers/ideal.py index cb0aa6e2..18ebf5d4 100644 --- a/custom_components/sat/manufacturers/ideal.py +++ b/custom_components/sat/manufacturers/ideal.py @@ -1,4 +1,4 @@ -from custom_components.sat.manufacturer import Manufacturer +from ..manufacturer import Manufacturer class Ideal(Manufacturer): diff --git a/custom_components/sat/manufacturers/immergas.py b/custom_components/sat/manufacturers/immergas.py index ccc0585d..08207733 100644 --- a/custom_components/sat/manufacturers/immergas.py +++ b/custom_components/sat/manufacturers/immergas.py @@ -1,4 +1,4 @@ -from custom_components.sat.manufacturer import Manufacturer +from ..manufacturer import Manufacturer class Immergas(Manufacturer): diff --git a/custom_components/sat/manufacturers/intergas.py b/custom_components/sat/manufacturers/intergas.py index 38ba8825..a6c98760 100644 --- a/custom_components/sat/manufacturers/intergas.py +++ b/custom_components/sat/manufacturers/intergas.py @@ -1,4 +1,4 @@ -from custom_components.sat.manufacturer import Manufacturer +from ..manufacturer import Manufacturer class Intergas(Manufacturer): diff --git a/custom_components/sat/manufacturers/nefit.py b/custom_components/sat/manufacturers/nefit.py index dc33e454..4f2b9669 100644 --- a/custom_components/sat/manufacturers/nefit.py +++ b/custom_components/sat/manufacturers/nefit.py @@ -1,4 +1,4 @@ -from custom_components.sat.manufacturer import Manufacturer +from ..manufacturer import Manufacturer class Nefit(Manufacturer): diff --git a/custom_components/sat/manufacturers/simulator.py b/custom_components/sat/manufacturers/simulator.py index 84a5934b..b4ae0293 100644 --- a/custom_components/sat/manufacturers/simulator.py +++ b/custom_components/sat/manufacturers/simulator.py @@ -1,4 +1,4 @@ -from custom_components.sat.manufacturer import Manufacturer +from ..manufacturer import Manufacturer class Simulator(Manufacturer): diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 6717e6a9..97bd0464 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -1,245 +1,112 @@ -from __future__ import annotations, annotations - import logging -from typing import TYPE_CHECKING, Mapping, Any +from abc import ABC, abstractmethod +from typing import Mapping, Any from homeassistant.components import mqtt -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.core import HomeAssistant, Event -from homeassistant.helpers import device_registry, entity_registry -from homeassistant.helpers.event import async_track_state_change_event - -from ..const import * -from ..coordinator import DeviceState, SatDataUpdateCoordinator, SatEntityCoordinator -from ..manufacturers.immergas import Immergas - -DATA_FLAME_ACTIVE = "flame" -DATA_DHW_SETPOINT = "TdhwSet" -DATA_CONTROL_SETPOINT = "TSet" -DATA_REL_MOD_LEVEL = "RelModLevel" -DATA_BOILER_TEMPERATURE = "Tboiler" -DATA_RETURN_TEMPERATURE = "Tret" -DATA_DHW_ENABLE = "domestichotwater" -DATA_CENTRAL_HEATING = "centralheating" -DATA_SLAVE_MEMBERID = "slave_memberid_code" -DATA_BOILER_CAPACITY = "MaxCapacityMinModLevel_hb_u8" -DATA_REL_MIN_MOD_LEVEL = "MaxCapacityMinModLevel_lb_u8" -DATA_REL_MIN_MOD_LEVELL = "MaxCapacityMinModLevell_lb_u8" -DATA_MAX_REL_MOD_LEVEL_SETTING = "MaxRelModLevelSetting" -DATA_DHW_SETPOINT_MINIMUM = "TdhwSetUBTdhwSetLB_value_lb" -DATA_DHW_SETPOINT_MAXIMUM = "TdhwSetUBTdhwSetLB_value_hb" - -if TYPE_CHECKING: - from ..climate import SatClimate +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.storage import Store + +from ..climate import SatClimate +from ..const import CONF_MQTT_TOPIC +from ..coordinator import SatDataUpdateCoordinator +from ..util import snake_case _LOGGER: logging.Logger = logging.getLogger(__name__) +STORAGE_VERSION = 1 -class SatMqttCoordinator(SatDataUpdateCoordinator, SatEntityCoordinator): - """Class to manage to fetch data from the OTGW Gateway using mqtt.""" + +class SatMqttCoordinator(ABC, SatDataUpdateCoordinator): + """Base class to manage fetching data using MQTT.""" def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], options: Mapping[str, Any] | None = None) -> None: super().__init__(hass, data, options) - self.data = {} + _LOGGER.debug(snake_case(f"{self.__class__.__name__}")) + _LOGGER.debug(device_id) - self._device = device_registry.async_get(hass).async_get(device_id) - self._node_id = list(self._device.identifiers)[0][1] + self.data = {} + self._device_id = device_id self._topic = data.get(CONF_MQTT_TOPIC) - - self._entity_registry = entity_registry.async_get(hass) - self._entities = entity_registry.async_entries_for_device(self._entity_registry, self._device.id) + self._store = Store(hass, STORAGE_VERSION, snake_case(f"{self.__class__.__name__}_{device_id}")) @property def device_id(self) -> str: - return self._node_id - - @property - def supports_setpoint_management(self): - return True - - @property - def supports_hot_water_setpoint_management(self): - return True - - def supports_maximum_setpoint_management(self): - return True - - @property - def supports_relative_modulation_management(self): - return True - - @property - def device_active(self) -> bool: - return self.get(BINARY_SENSOR_DOMAIN, DATA_CENTRAL_HEATING) == DeviceState.ON - - @property - def flame_active(self) -> bool: - return self.get(BINARY_SENSOR_DOMAIN, DATA_FLAME_ACTIVE) == DeviceState.ON - - @property - def hot_water_active(self) -> bool: - return self.get(BINARY_SENSOR_DOMAIN, DATA_DHW_ENABLE) == DeviceState.ON - - @property - def setpoint(self) -> float | None: - if (setpoint := self.get(SENSOR_DOMAIN, DATA_CONTROL_SETPOINT)) is not None: - return float(setpoint) - - return None - - @property - def hot_water_setpoint(self) -> float | None: - if (setpoint := self.get(SENSOR_DOMAIN, DATA_DHW_SETPOINT)) is not None: - return float(setpoint) - - return super().hot_water_setpoint - - @property - def minimum_hot_water_setpoint(self) -> float: - if (setpoint := self.get(SENSOR_DOMAIN, DATA_DHW_SETPOINT_MINIMUM)) is not None: - return float(setpoint) - - return super().minimum_hot_water_setpoint - - @property - def maximum_hot_water_setpoint(self) -> float | None: - if (setpoint := self.get(SENSOR_DOMAIN, DATA_DHW_SETPOINT_MAXIMUM)) is not None: - return float(setpoint) - - return super().maximum_hot_water_setpoint - - @property - def boiler_temperature(self) -> float | None: - if (value := self.get(SENSOR_DOMAIN, DATA_BOILER_TEMPERATURE)) is not None: - return float(value) - - return super().boiler_temperature - - @property - def return_temperature(self) -> float | None: - if (value := self.get(SENSOR_DOMAIN, DATA_RETURN_TEMPERATURE)) is not None: - return float(value) - - return super().return_temperature - - @property - def relative_modulation_value(self) -> float | None: - if (value := self.get(SENSOR_DOMAIN, DATA_REL_MOD_LEVEL)) is not None: - return float(value) - - return super().relative_modulation_value - - @property - def boiler_capacity(self) -> float | None: - if (value := self.get(SENSOR_DOMAIN, DATA_BOILER_CAPACITY)) is not None: - return float(value) - - return super().boiler_capacity - - @property - def minimum_relative_modulation_value(self) -> float | None: - if (value := self.get(SENSOR_DOMAIN, DATA_REL_MIN_MOD_LEVEL)) is not None: - return float(value) - - # Legacy - if (value := self.get(SENSOR_DOMAIN, DATA_REL_MIN_MOD_LEVELL)) is not None: - return float(value) - - return super().minimum_relative_modulation_value - - @property - def maximum_relative_modulation_value(self) -> float | None: - if (value := self.get(SENSOR_DOMAIN, DATA_MAX_REL_MOD_LEVEL_SETTING)) is not None: - return float(value) - - return super().maximum_relative_modulation_value - - @property - def member_id(self) -> int | None: - if (value := self.get(SENSOR_DOMAIN, DATA_SLAVE_MEMBERID)) is not None: - return int(value) - - return None - - async def boot(self) -> SatMqttCoordinator: - await self._send_command("PM=3") - await self._send_command("PM=48") - - return self + return self._device_id async def async_added_to_hass(self, climate: SatClimate) -> None: await mqtt.async_wait_for_mqtt_client(self.hass) - # Create a list of entities that we track - entities = list(filter(lambda entity: entity is not None, [ - self._get_entity_id(BINARY_SENSOR_DOMAIN, DATA_CENTRAL_HEATING), - self._get_entity_id(BINARY_SENSOR_DOMAIN, DATA_FLAME_ACTIVE), - self._get_entity_id(BINARY_SENSOR_DOMAIN, DATA_DHW_ENABLE), - - self._get_entity_id(SENSOR_DOMAIN, DATA_DHW_SETPOINT), - self._get_entity_id(SENSOR_DOMAIN, DATA_CONTROL_SETPOINT), - self._get_entity_id(SENSOR_DOMAIN, DATA_REL_MOD_LEVEL), - self._get_entity_id(SENSOR_DOMAIN, DATA_BOILER_TEMPERATURE), - self._get_entity_id(SENSOR_DOMAIN, DATA_BOILER_CAPACITY), - self._get_entity_id(SENSOR_DOMAIN, DATA_REL_MIN_MOD_LEVEL), - self._get_entity_id(SENSOR_DOMAIN, DATA_REL_MIN_MOD_LEVELL), - self._get_entity_id(SENSOR_DOMAIN, DATA_MAX_REL_MOD_LEVEL_SETTING), - self._get_entity_id(SENSOR_DOMAIN, DATA_DHW_SETPOINT_MINIMUM), - self._get_entity_id(SENSOR_DOMAIN, DATA_DHW_SETPOINT_MAXIMUM), - ])) - - # Track those entities so the coordinator can be updated when something changes - async_track_state_change_event(self.hass, entities, self.async_state_change_event) + for key in self.get_tracked_entities(): + await mqtt.async_subscribe( + self.hass, + self._get_topic_for_subscription(key), + self._create_message_handler(key) + ) - await super().async_added_to_hass(climate) + await self.boot() - async def async_state_change_event(self, _event: Event): - if self._listeners: - self._schedule_refresh() + await super().async_added_to_hass(climate) - self.async_update_listeners() + async def async_notify_listeners(self): + """Notify listeners of an update asynchronously.""" + # Make sure we do not spam + self._async_unsub_refresh() + self._debounced_refresh.async_cancel() - async def async_set_control_setpoint(self, value: float) -> None: - await self._send_command(f"CS={value}") + # Save the updated data to persistent storage + await self._save_data() - await super().async_set_control_setpoint(value) + # Inform the listeners that we are updated + self.async_update_listeners() - async def async_set_control_hot_water_setpoint(self, value: float) -> None: - await self._send_command(f"SW={value}") + async def _load_stored_data(self) -> None: + """Load the data from persistent storage.""" + if stored_data := await self._store.async_load(): + self.data.update(stored_data) - await super().async_set_control_hot_water_setpoint(value) + async def _save_data(self) -> None: + """Save the data to persistent storage.""" + await self._store.async_save(self.data) - async def async_set_control_thermostat_setpoint(self, value: float) -> None: - await self._send_command(f"TC={value}") + @abstractmethod + def get_tracked_entities(self) -> list[str]: + """Method to be overridden in subclasses to provide specific entities to track.""" + pass - await super().async_set_control_thermostat_setpoint(value) + @abstractmethod + def _get_topic_for_subscription(self, key: str) -> str: + """Method to be overridden in subclasses to provide a specific topic for subscribing.""" + pass - async def async_set_heater_state(self, state: DeviceState) -> None: - await self._send_command(f"CH={1 if state == DeviceState.ON else 0}") + @abstractmethod + def _get_topic_for_publishing(self) -> str: + """Method to be overridden in subclasses to provide a specific topic for publishing.""" + pass - await super().async_set_heater_state(state) + @abstractmethod + async def boot(self) -> None: + """Method to be overridden in subclasses to provide specific boot functionality.""" + pass - async def async_set_control_max_relative_modulation(self, value: int) -> None: - if isinstance(self.manufacturer, Immergas): - await self._send_command(f"TP=11:12={min(value, 80)}") + def _create_message_handler(self, key: str): + """Create a message handler to properly schedule updates.""" - await self._send_command(f"MM={value}") + @callback + def message_handler(msg): + """Handle received MQTT message and schedule data update.""" + _LOGGER.debug(f"Receiving '{key}'='{msg.payload}' from MQTT.") - await super().async_set_control_max_relative_modulation(value) + # Store the new value + self.data[key] = msg.payload - async def async_set_control_max_setpoint(self, value: float) -> None: - await self._send_command(f"SH={value}") + # Schedule the update so our entities are updated + self.hass.async_create_task(self.async_notify_listeners()) - await super().async_set_control_max_setpoint(value) + return message_handler - def _get_entity_id(self, domain: str, key: str): - return self._entity_registry.async_get_entity_id(domain, MQTT_DOMAIN, f"{self._node_id}-{key}") + async def _publish_command(self, payload: str): + _LOGGER.debug(f"Publishing '{payload}' to MQTT.") - async def _send_command(self, payload: str): if not self._simulation: - await mqtt.async_publish(self.hass, f"{self._topic}/set/{self._node_id}/command", payload) - - _LOGGER.debug(f"Publishing '{payload}' to MQTT.") + await mqtt.async_publish(self.hass, self._get_topic_for_publishing(), payload) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py new file mode 100644 index 00000000..70fa724a --- /dev/null +++ b/custom_components/sat/mqtt/ems.py @@ -0,0 +1,165 @@ +from __future__ import annotations + +import logging + +from homeassistant.components import mqtt +from homeassistant.core import Event + +from . import SatMqttCoordinator +from ..coordinator import DeviceState + +DATA_FLAME_ACTIVE = "burngas" +DATA_DHW_SETPOINT = "dhw/seltemp" +DATA_CONTROL_SETPOINT = "selflowtemp" +DATA_REL_MOD_LEVEL = "curburnpow" +DATA_BOILER_TEMPERATURE = "curflowtemp" +DATA_RETURN_TEMPERATURE = "rettemp" + +DATA_DHW_ENABLE = "tapwateractive" +DATA_CENTRAL_HEATING = "heatingactive" +DATA_BOILER_CAPACITY = "nompower" + +DATA_REL_MIN_MOD_LEVEL = "burnminnpower" +DATA_MAX_REL_MOD_LEVEL_SETTING = "burnmaxpower" + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +class SatEmsMqttCoordinator(SatMqttCoordinator): + """Class to manage fetching data from the OTGW Gateway using MQTT.""" + + @property + def supports_setpoint_management(self) -> bool: + return True + + @property + def supports_hot_water_setpoint_management(self) -> bool: + return True + + @property + def supports_maximum_setpoint_management(self) -> bool: + return True + + @property + def supports_relative_modulation_management(self) -> bool: + return True + + @property + def device_active(self) -> bool: + return self.data.get(DATA_CENTRAL_HEATING) + + @property + def flame_active(self) -> bool: + return self.data.get(DATA_FLAME_ACTIVE) + + @property + def hot_water_active(self) -> bool: + return self.data.get(DATA_DHW_ENABLE) + + @property + def setpoint(self) -> float | None: + return self.data.get(DATA_CONTROL_SETPOINT) + + @property + def hot_water_setpoint(self) -> float | None: + return self.data.get(DATA_DHW_SETPOINT) + + @property + def boiler_temperature(self) -> float | None: + return self.data.get(DATA_BOILER_TEMPERATURE) + + @property + def return_temperature(self) -> float | None: + return self.data.get(DATA_RETURN_TEMPERATURE) + + @property + def relative_modulation_value(self) -> float | None: + return self.data.get(DATA_REL_MOD_LEVEL) + + @property + def boiler_capacity(self) -> float | None: + value = self.data.get(DATA_BOILER_CAPACITY) + return float(value) if value is not None else super().boiler_capacity + + @property + def minimum_relative_modulation_value(self) -> float | None: + value = self.data.get(DATA_REL_MIN_MOD_LEVEL) + return float(value) if value is not None else super().minimum_relative_modulation_value + + @property + def maximum_relative_modulation_value(self) -> float | None: + value = self.data.get(DATA_MAX_REL_MOD_LEVEL_SETTING) + return float(value) if value is not None else super().maximum_relative_modulation_value + + @property + def member_id(self) -> int | None: + # Not supported (yet) + return None + + async def boot(self) -> SatMqttCoordinator: + # Nothing needs to be booted (yet) + return self + + def get_tracked_entities(self) -> list[str]: + return [ + DATA_CENTRAL_HEATING, + DATA_FLAME_ACTIVE, + DATA_DHW_ENABLE, + DATA_DHW_SETPOINT, + DATA_CONTROL_SETPOINT, + DATA_REL_MOD_LEVEL, + DATA_BOILER_TEMPERATURE, + DATA_BOILER_CAPACITY, + DATA_REL_MIN_MOD_LEVEL, + DATA_MAX_REL_MOD_LEVEL_SETTING, + ] + + async def async_state_change_event(self, _event: Event) -> None: + if self._listeners: + self._schedule_refresh() + + self.async_update_listeners() + + async def async_set_control_setpoint(self, value: float) -> None: + await self._publish_command(f'{"cmd": "selflowtemp", "value": {0 if value == 10 else value}}') + await super().async_set_control_setpoint(value) + + async def async_set_control_hot_water_setpoint(self, value: float) -> None: + await self._publish_command(f'{"cmd": "dhw/seltemp", "value": {value}}') + await super().async_set_control_hot_water_setpoint(value) + + async def async_set_control_thermostat_setpoint(self, value: float) -> None: + # Not supported (yet) + await super().async_set_control_thermostat_setpoint(value) + + async def async_set_heater_state(self, state: DeviceState) -> None: + if state == DeviceState.OFF: + await self.async_set_control_setpoint(0) + + await super().async_set_heater_state(state) + + async def async_set_control_max_relative_modulation(self, value: int) -> None: + await self._publish_command(f'{"cmd": "burnmaxpower", "value": {value}}') + + await super().async_set_control_max_relative_modulation(value) + + async def async_set_control_max_setpoint(self, value: float) -> None: + await self._publish_command(f'{"cmd": "heatingtemp", "value": {value}}') + + await super().async_set_control_max_setpoint(value) + + def _get_topic_for_subscription(self, key: str) -> str: + return f"{self._topic}/ems-esp/{key}" + + def _get_topic_for_publishing(self) -> str: + return f"{self._topic}/ems-esp/boiler" + + def _async_subscribe(self, key: str) -> None: + topic = self._get_topic_for_subscription(key) + mqtt.async_subscribe(self.hass, topic, lambda msg: self._handle_message(key, msg.payload)) + + def _handle_message(self, key: str, value: str) -> None: + try: + self.data[key] = float(value) if value.replace('.', '', 1).isdigit() else value + except ValueError: + _LOGGER.warning(f"Unable to parse value '{value}' for key '{key}'") diff --git a/custom_components/sat/mqtt/opentherm.py b/custom_components/sat/mqtt/opentherm.py new file mode 100644 index 00000000..5d328153 --- /dev/null +++ b/custom_components/sat/mqtt/opentherm.py @@ -0,0 +1,198 @@ +from __future__ import annotations, annotations + +import logging + +from . import SatMqttCoordinator +from ..coordinator import DeviceState +from ..manufacturers.immergas import Immergas + +STATE_ON = "ON" + +DATA_FLAME_ACTIVE = "flame" +DATA_DHW_SETPOINT = "TdhwSet" +DATA_CONTROL_SETPOINT = "TSet" +DATA_REL_MOD_LEVEL = "RelModLevel" +DATA_BOILER_TEMPERATURE = "Tboiler" +DATA_RETURN_TEMPERATURE = "Tret" +DATA_DHW_ENABLE = "domestichotwater" +DATA_CENTRAL_HEATING = "centralheating" +DATA_SLAVE_MEMBERID = "slave_memberid_code" +DATA_BOILER_CAPACITY = "MaxCapacityMinModLevel_hb_u8" +DATA_REL_MIN_MOD_LEVEL = "MaxCapacityMinModLevel_lb_u8" +DATA_REL_MIN_MOD_LEVELL = "MaxCapacityMinModLevell_lb_u8" +DATA_MAX_REL_MOD_LEVEL_SETTING = "MaxRelModLevelSetting" +DATA_DHW_SETPOINT_MINIMUM = "TdhwSetUBTdhwSetLB_value_lb" +DATA_DHW_SETPOINT_MAXIMUM = "TdhwSetUBTdhwSetLB_value_hb" + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +class SatOpenThermMqttCoordinator(SatMqttCoordinator): + """Class to manage to fetch data from the OTGW Gateway using mqtt.""" + + @property + def supports_setpoint_management(self): + return True + + @property + def supports_hot_water_setpoint_management(self): + return True + + def supports_maximum_setpoint_management(self): + return True + + @property + def supports_relative_modulation_management(self): + return True + + @property + def device_active(self) -> bool: + return self.data.get(DATA_CENTRAL_HEATING) == STATE_ON + + @property + def flame_active(self) -> bool: + return self.data.get(DATA_FLAME_ACTIVE) == STATE_ON + + @property + def hot_water_active(self) -> bool: + return self.data.get(DATA_DHW_ENABLE) == STATE_ON + + @property + def setpoint(self) -> float | None: + if (setpoint := self.data.get(DATA_CONTROL_SETPOINT)) is not None: + return float(setpoint) + + return None + + @property + def hot_water_setpoint(self) -> float | None: + if (setpoint := self.data.get(DATA_DHW_SETPOINT)) is not None: + return float(setpoint) + + return super().hot_water_setpoint + + @property + def minimum_hot_water_setpoint(self) -> float: + if (setpoint := self.data.get(DATA_DHW_SETPOINT_MINIMUM)) is not None: + return float(setpoint) + + return super().minimum_hot_water_setpoint + + @property + def maximum_hot_water_setpoint(self) -> float | None: + if (setpoint := self.data.get(DATA_DHW_SETPOINT_MAXIMUM)) is not None: + return float(setpoint) + + return super().maximum_hot_water_setpoint + + @property + def boiler_temperature(self) -> float | None: + if (value := self.data.get(DATA_BOILER_TEMPERATURE)) is not None: + return float(value) + + return super().boiler_temperature + + @property + def return_temperature(self) -> float | None: + if (value := self.data.get(DATA_RETURN_TEMPERATURE)) is not None: + return float(value) + + return super().return_temperature + + @property + def relative_modulation_value(self) -> float | None: + if (value := self.data.get(DATA_REL_MOD_LEVEL)) is not None: + return float(value) + + return super().relative_modulation_value + + @property + def boiler_capacity(self) -> float | None: + if (value := self.data.get(DATA_BOILER_CAPACITY)) is not None: + return float(value) + + return super().boiler_capacity + + @property + def minimum_relative_modulation_value(self) -> float | None: + if (value := self.data.get(DATA_REL_MIN_MOD_LEVEL)) is not None: + return float(value) + + # Legacy + if (value := self.data.get(DATA_REL_MIN_MOD_LEVELL)) is not None: + return float(value) + + return super().minimum_relative_modulation_value + + @property + def maximum_relative_modulation_value(self) -> float | None: + if (value := self.data.get(DATA_MAX_REL_MOD_LEVEL_SETTING)) is not None: + return float(value) + + return super().maximum_relative_modulation_value + + @property + def member_id(self) -> int | None: + if (value := self.data.get(DATA_SLAVE_MEMBERID)) is not None: + return int(value) + + return None + + async def boot(self) -> None: + await self._publish_command("PM=3") + await self._publish_command("PM=48") + + def get_tracked_entities(self) -> list[str]: + return [ + DATA_CENTRAL_HEATING, + DATA_FLAME_ACTIVE, + DATA_DHW_ENABLE, + DATA_DHW_SETPOINT, + DATA_CONTROL_SETPOINT, + DATA_REL_MOD_LEVEL, + DATA_BOILER_TEMPERATURE, + DATA_BOILER_CAPACITY, + DATA_REL_MIN_MOD_LEVEL, + DATA_MAX_REL_MOD_LEVEL_SETTING, + DATA_DHW_SETPOINT_MINIMUM, + DATA_DHW_SETPOINT_MAXIMUM, + ] + + def _get_topic_for_subscription(self, key: str) -> str: + return f"{self._topic}/value/{self._device_id}/{key}" + + def _get_topic_for_publishing(self) -> str: + return f"{self._topic}/set/{self._device_id}/command" + + async def async_set_control_setpoint(self, value: float) -> None: + await self._publish_command(f"CS={value}") + + await super().async_set_control_setpoint(value) + + async def async_set_control_hot_water_setpoint(self, value: float) -> None: + await self._publish_command(f"SW={value}") + + await super().async_set_control_hot_water_setpoint(value) + + async def async_set_control_thermostat_setpoint(self, value: float) -> None: + await self._publish_command(f"TC={value}") + + await super().async_set_control_thermostat_setpoint(value) + + async def async_set_heater_state(self, state: DeviceState) -> None: + await self._publish_command(f"CH={1 if state == DeviceState.ON else 0}") + + await super().async_set_heater_state(state) + + async def async_set_control_max_relative_modulation(self, value: int) -> None: + if isinstance(self.manufacturer, Immergas): + await self._publish_command(f"TP=11:12={min(value, 80)}") + + await self._publish_command(f"MM={value}") + + await super().async_set_control_max_relative_modulation(value) + + async def async_set_control_max_setpoint(self, value: float) -> None: + await self._publish_command(f"SH={value}") + + await super().async_set_control_max_setpoint(value) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 248e2b28..98b2399c 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -1,8 +1,8 @@ import asyncio import logging -from custom_components.sat.const import * -from custom_components.sat.coordinator import DeviceState, SatDataUpdateCoordinator +from .const import OVERSHOOT_PROTECTION_SETPOINT, MINIMUM_SETPOINT, DEADBAND, MAXIMUM_RELATIVE_MOD +from .coordinator import DeviceState, SatDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/sat/relative_modulation.py b/custom_components/sat/relative_modulation.py index 103184a1..59b43533 100644 --- a/custom_components/sat/relative_modulation.py +++ b/custom_components/sat/relative_modulation.py @@ -1,8 +1,8 @@ from enum import Enum -from custom_components.sat import MINIMUM_SETPOINT, HEATING_SYSTEM_HEAT_PUMP -from custom_components.sat.coordinator import SatDataUpdateCoordinator -from custom_components.sat.pwm import PWMState +from . import MINIMUM_SETPOINT, HEATING_SYSTEM_HEAT_PUMP +from .coordinator import SatDataUpdateCoordinator +from .pwm import PWMState # Enum to represent different states of relative modulation diff --git a/custom_components/sat/serial/__init__.py b/custom_components/sat/serial/__init__.py index 38f45741..075b8ff9 100644 --- a/custom_components/sat/serial/__init__.py +++ b/custom_components/sat/serial/__init__.py @@ -166,6 +166,11 @@ async def async_connect(self) -> SatSerialCoordinator: return self + async def async_added_to_hass(self, climate: SatClimate) -> None: + await self.async_connect() + + await super().async_added_to_hass(climate) + async def async_will_remove_from_hass(self, climate: SatClimate) -> None: self._api.unsubscribe(self.async_set_updated_data) diff --git a/custom_components/sat/util.py b/custom_components/sat/util.py index c5f80532..bcd8fe62 100644 --- a/custom_components/sat/util.py +++ b/custom_components/sat/util.py @@ -1,4 +1,6 @@ from re import sub +from types import MappingProxyType +from typing import Any from homeassistant.util import dt @@ -77,7 +79,7 @@ def create_heating_curve_controller(config_data, config_options) -> HeatingCurve return HeatingCurve(heating_system=heating_system, coefficient=coefficient, version=version) -def create_pwm_controller(heating_curve: HeatingCurve, config_data, config_options) -> PWM | None: +def create_pwm_controller(heating_curve: HeatingCurve, config_data: MappingProxyType[str, Any], config_options: MappingProxyType[str, Any]) -> PWM | None: """Create and return a PWM controller instance with the given configuration options.""" # Extract the configuration options max_duty_cycles = int(config_options.get(CONF_CYCLES_PER_HOUR)) @@ -96,8 +98,16 @@ def create_minimum_setpoint_controller(config_data, config_options) -> MinimumSe return MinimumSetpoint(configured_minimum_setpoint=minimum_setpoint, adjustment_factor=adjustment_factor) -def snake_case(s): +def snake_case(value: str): return '_'.join( sub('([A-Z][a-z]+)', r' \1', sub('([A-Z]+)', r' \1', - s.replace('-', ' '))).split()).lower() + value.replace('-', ' '))).split()).lower() + + +def float_value(value) -> float: + """Helper method to convert a value to float, handling possible errors.""" + try: + return float(value) + except (TypeError, ValueError): + return 0 From 243954e5d7e7c448e784cb959ddd50d6b08928fd Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 6 Dec 2024 19:12:07 +0100 Subject: [PATCH 125/213] Add some sanity checks and remove obsolete code --- custom_components/sat/area.py | 3 ++- custom_components/sat/mqtt/ems.py | 23 ++++++----------------- custom_components/sat/mqtt/opentherm.py | 12 ++++++------ 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/custom_components/sat/area.py b/custom_components/sat/area.py index 1db79f0a..b94925bf 100644 --- a/custom_components/sat/area.py +++ b/custom_components/sat/area.py @@ -129,7 +129,8 @@ def __init__(self, areas: list[Area]): def update(self, boiler_temperature: float) -> None: for area in self.areas: - area.pid.update(area.error, area.heating_curve.value, boiler_temperature) + if area.error is not None: + area.pid.update(area.error, area.heating_curve.value, boiler_temperature) def reset(self) -> None: """Reset PID controllers for all areas.""" diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index 70fa724a..27ec599b 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -2,7 +2,6 @@ import logging -from homeassistant.components import mqtt from homeassistant.core import Event from . import SatMqttCoordinator @@ -121,11 +120,11 @@ async def async_state_change_event(self, _event: Event) -> None: self.async_update_listeners() async def async_set_control_setpoint(self, value: float) -> None: - await self._publish_command(f'{"cmd": "selflowtemp", "value": {0 if value == 10 else value}}') + await self._publish_command(f'{{"cmd": "selflowtemp", "value": {0 if value == 10 else value}}}') await super().async_set_control_setpoint(value) async def async_set_control_hot_water_setpoint(self, value: float) -> None: - await self._publish_command(f'{"cmd": "dhw/seltemp", "value": {value}}') + await self._publish_command(f'{{"cmd": "dhw/seltemp", "value": {value}}}') await super().async_set_control_hot_water_setpoint(value) async def async_set_control_thermostat_setpoint(self, value: float) -> None: @@ -139,27 +138,17 @@ async def async_set_heater_state(self, state: DeviceState) -> None: await super().async_set_heater_state(state) async def async_set_control_max_relative_modulation(self, value: int) -> None: - await self._publish_command(f'{"cmd": "burnmaxpower", "value": {value}}') + await self._publish_command(f'{{"cmd": "burnmaxpower", "value": {value}}}') await super().async_set_control_max_relative_modulation(value) async def async_set_control_max_setpoint(self, value: float) -> None: - await self._publish_command(f'{"cmd": "heatingtemp", "value": {value}}') + await self._publish_command(f'{{"cmd": "heatingtemp", "value": {value}}}') await super().async_set_control_max_setpoint(value) def _get_topic_for_subscription(self, key: str) -> str: - return f"{self._topic}/ems-esp/{key}" + return f"{self._topic}/{self.device_id}/{key}" def _get_topic_for_publishing(self) -> str: - return f"{self._topic}/ems-esp/boiler" - - def _async_subscribe(self, key: str) -> None: - topic = self._get_topic_for_subscription(key) - mqtt.async_subscribe(self.hass, topic, lambda msg: self._handle_message(key, msg.payload)) - - def _handle_message(self, key: str, value: str) -> None: - try: - self.data[key] = float(value) if value.replace('.', '', 1).isdigit() else value - except ValueError: - _LOGGER.warning(f"Unable to parse value '{value}' for key '{key}'") + return f"{self._topic}/{self.device_id}/boiler" diff --git a/custom_components/sat/mqtt/opentherm.py b/custom_components/sat/mqtt/opentherm.py index 5d328153..ffa30cf7 100644 --- a/custom_components/sat/mqtt/opentherm.py +++ b/custom_components/sat/mqtt/opentherm.py @@ -158,12 +158,6 @@ def get_tracked_entities(self) -> list[str]: DATA_DHW_SETPOINT_MAXIMUM, ] - def _get_topic_for_subscription(self, key: str) -> str: - return f"{self._topic}/value/{self._device_id}/{key}" - - def _get_topic_for_publishing(self) -> str: - return f"{self._topic}/set/{self._device_id}/command" - async def async_set_control_setpoint(self, value: float) -> None: await self._publish_command(f"CS={value}") @@ -196,3 +190,9 @@ async def async_set_control_max_setpoint(self, value: float) -> None: await self._publish_command(f"SH={value}") await super().async_set_control_max_setpoint(value) + + def _get_topic_for_subscription(self, key: str) -> str: + return f"{self._topic}/value/{self._device_id}/{key}" + + def _get_topic_for_publishing(self) -> str: + return f"{self._topic}/set/{self._device_id}/command" From 88feb5e0c383a24949472054a8dc2c5e0cafef35 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 6 Dec 2024 19:13:16 +0100 Subject: [PATCH 126/213] Typo? --- custom_components/sat/esphome/__init__.py | 1 + custom_components/sat/fake/__init__.py | 1 + custom_components/sat/mqtt/opentherm.py | 1 + 3 files changed, 3 insertions(+) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index 544b8054..fda68bd5 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -70,6 +70,7 @@ def supports_setpoint_management(self): def supports_hot_water_setpoint_management(self): return True + @property def supports_maximum_setpoint_management(self): return True diff --git a/custom_components/sat/fake/__init__.py b/custom_components/sat/fake/__init__.py index fa7e62ba..83371a85 100644 --- a/custom_components/sat/fake/__init__.py +++ b/custom_components/sat/fake/__init__.py @@ -71,6 +71,7 @@ def supports_hot_water_setpoint_management(self): return self.config.supports_hot_water_setpoint_management + @property def supports_maximum_setpoint_management(self): if self.config is None: return super().supports_maximum_setpoint_management diff --git a/custom_components/sat/mqtt/opentherm.py b/custom_components/sat/mqtt/opentherm.py index ffa30cf7..b4fba33b 100644 --- a/custom_components/sat/mqtt/opentherm.py +++ b/custom_components/sat/mqtt/opentherm.py @@ -38,6 +38,7 @@ def supports_setpoint_management(self): def supports_hot_water_setpoint_management(self): return True + @property def supports_maximum_setpoint_management(self): return True From bcaa021e8a0f2c98bfda3a225a7f568ef906c8e9 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 6 Dec 2024 19:13:42 +0100 Subject: [PATCH 127/213] Fix the command format for EMS --- custom_components/sat/mqtt/ems.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index 27ec599b..4a8f8528 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -148,7 +148,7 @@ async def async_set_control_max_setpoint(self, value: float) -> None: await super().async_set_control_max_setpoint(value) def _get_topic_for_subscription(self, key: str) -> str: - return f"{self._topic}/{self.device_id}/{key}" + return f"{self._topic}/{key}" def _get_topic_for_publishing(self) -> str: - return f"{self._topic}/{self.device_id}/boiler" + return f"{self._topic}/boiler" From 09b97547c70965adf23ad5de6c47e99f6ed60cad Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 6 Dec 2024 19:20:35 +0100 Subject: [PATCH 128/213] Sanity checks --- custom_components/sat/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 82e89def..0e3bfdc5 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -653,7 +653,7 @@ async def _async_climate_changed(self, event: Event) -> None: await self._async_control_pid(True) # If the current temperature has changed, update the PID controller - elif not hasattr(new_state.attributes, SENSOR_TEMPERATURE_ID) and new_attrs.get("current_temperature") != old_attrs.get("current_temperature"): + elif SENSOR_TEMPERATURE_ID not in new_state.attributes and new_attrs.get("current_temperature") != old_attrs.get("current_temperature"): await self._async_control_pid() if (self._rooms is not None and new_state.entity_id not in self._rooms) or self.preset_mode in [PRESET_HOME, PRESET_COMFORT]: From f520bc1fceacf3b545f2efa76ae10120b28032d1 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 6 Dec 2024 19:22:11 +0100 Subject: [PATCH 129/213] Make sure we also cast some values and proper name constants --- custom_components/sat/mqtt/ems.py | 6 +++--- custom_components/sat/mqtt/opentherm.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index 4a8f8528..43d38c5d 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -45,15 +45,15 @@ def supports_relative_modulation_management(self) -> bool: @property def device_active(self) -> bool: - return self.data.get(DATA_CENTRAL_HEATING) + return bool(self.data.get(DATA_CENTRAL_HEATING)) @property def flame_active(self) -> bool: - return self.data.get(DATA_FLAME_ACTIVE) + return bool(self.data.get(DATA_FLAME_ACTIVE)) @property def hot_water_active(self) -> bool: - return self.data.get(DATA_DHW_ENABLE) + return bool(self.data.get(DATA_DHW_ENABLE)) @property def setpoint(self) -> float | None: diff --git a/custom_components/sat/mqtt/opentherm.py b/custom_components/sat/mqtt/opentherm.py index b4fba33b..ed8db4f6 100644 --- a/custom_components/sat/mqtt/opentherm.py +++ b/custom_components/sat/mqtt/opentherm.py @@ -19,7 +19,7 @@ DATA_SLAVE_MEMBERID = "slave_memberid_code" DATA_BOILER_CAPACITY = "MaxCapacityMinModLevel_hb_u8" DATA_REL_MIN_MOD_LEVEL = "MaxCapacityMinModLevel_lb_u8" -DATA_REL_MIN_MOD_LEVELL = "MaxCapacityMinModLevell_lb_u8" +DATA_REL_MIN_MOD_LEVEL_LEGACY = "MaxCapacityMinModLevell_lb_u8" DATA_MAX_REL_MOD_LEVEL_SETTING = "MaxRelModLevelSetting" DATA_DHW_SETPOINT_MINIMUM = "TdhwSetUBTdhwSetLB_value_lb" DATA_DHW_SETPOINT_MAXIMUM = "TdhwSetUBTdhwSetLB_value_hb" @@ -120,7 +120,7 @@ def minimum_relative_modulation_value(self) -> float | None: return float(value) # Legacy - if (value := self.data.get(DATA_REL_MIN_MOD_LEVELL)) is not None: + if (value := self.data.get(DATA_REL_MIN_MOD_LEVEL_LEGACY)) is not None: return float(value) return super().minimum_relative_modulation_value From 87c724389cb2b20b487375114e614f49b41c86a2 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 6 Dec 2024 19:33:50 +0100 Subject: [PATCH 130/213] Add type support (for now) --- custom_components/sat/config_flow.py | 16 +++++++++++++++- custom_components/sat/const.py | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index f7cb1a24..c60ba023 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -120,7 +120,15 @@ async def async_step_mosquitto(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: self.data.update(_user_input) - self.data[CONF_MODE] = MODE_MQTT_OPENTHERM + + if self.data[CONF_TYPE] == MODE_MQTT_OPENTHERM: + self.data[CONF_MODE] = MODE_MQTT_OPENTHERM + + if self.data[CONF_TYPE] == MODE_MQTT_EMS: + self.data[CONF_MODE] = MODE_MQTT_EMS + + # Since we do not require this to be stored + del self.data[CONF_TYPE] if not await mqtt.async_wait_for_mqtt_client(self.hass): self.errors["base"] = "mqtt_component" @@ -133,6 +141,12 @@ async def async_step_mosquitto(self, _user_input: dict[str, Any] | None = None): last_step=False, errors=self.errors, data_schema=vol.Schema({ + vol.Required(CONF_TYPE): selector.SelectSelector( + selector.SelectSelectorConfig(mode=SelectSelectorMode.DROPDOWN, options=[ + selector.SelectOptionDict(value=MODE_MQTT_OPENTHERM, label="OpenTherm Gateway"), + selector.SelectOptionDict(value=MODE_MQTT_EMS, label="EMS-ESP"), + ]) + ), vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_MQTT_TOPIC, default=OPTIONS_DEFAULTS[CONF_MQTT_TOPIC]): str, vol.Required(CONF_DEVICE, default=self.data.get(CONF_DEVICE, "otgw-XXXXXXXXXXXX")): str, diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index 394368ba..f6bd4d56 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -27,6 +27,7 @@ # Configuration and options CONF_MODE = "mode" CONF_NAME = "name" +CONF_TYPE = "type" CONF_DEVICE = "device" CONF_CYCLES_PER_HOUR = "cycles_per_hour" CONF_SIMULATED_HEATING = "simulated_heating" From 80ab9c68d5c56c6187ad21e11c254a02a86f488f Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Fri, 6 Dec 2024 19:44:49 +0100 Subject: [PATCH 131/213] Some more casting --- custom_components/sat/mqtt/ems.py | 28 +++++++++------------------- custom_components/sat/util.py | 6 +++--- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index 43d38c5d..1f19df78 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -2,10 +2,9 @@ import logging -from homeassistant.core import Event - from . import SatMqttCoordinator from ..coordinator import DeviceState +from ..util import float_value DATA_FLAME_ACTIVE = "burngas" DATA_DHW_SETPOINT = "dhw/seltemp" @@ -57,38 +56,35 @@ def hot_water_active(self) -> bool: @property def setpoint(self) -> float | None: - return self.data.get(DATA_CONTROL_SETPOINT) + return float_value(self.data.get(DATA_CONTROL_SETPOINT)) @property def hot_water_setpoint(self) -> float | None: - return self.data.get(DATA_DHW_SETPOINT) + return float_value(self.data.get(DATA_DHW_SETPOINT)) @property def boiler_temperature(self) -> float | None: - return self.data.get(DATA_BOILER_TEMPERATURE) + return float_value(self.data.get(DATA_BOILER_TEMPERATURE)) @property def return_temperature(self) -> float | None: - return self.data.get(DATA_RETURN_TEMPERATURE) + return float_value(self.data.get(DATA_RETURN_TEMPERATURE)) @property def relative_modulation_value(self) -> float | None: - return self.data.get(DATA_REL_MOD_LEVEL) + return float_value(self.data.get(DATA_REL_MOD_LEVEL)) @property def boiler_capacity(self) -> float | None: - value = self.data.get(DATA_BOILER_CAPACITY) - return float(value) if value is not None else super().boiler_capacity + return float_value(self.data.get(DATA_BOILER_CAPACITY)) @property def minimum_relative_modulation_value(self) -> float | None: - value = self.data.get(DATA_REL_MIN_MOD_LEVEL) - return float(value) if value is not None else super().minimum_relative_modulation_value + return float_value(self.data.get(DATA_REL_MIN_MOD_LEVEL)) @property def maximum_relative_modulation_value(self) -> float | None: - value = self.data.get(DATA_MAX_REL_MOD_LEVEL_SETTING) - return float(value) if value is not None else super().maximum_relative_modulation_value + return float_value(self.data.get(DATA_MAX_REL_MOD_LEVEL_SETTING)) @property def member_id(self) -> int | None: @@ -113,12 +109,6 @@ def get_tracked_entities(self) -> list[str]: DATA_MAX_REL_MOD_LEVEL_SETTING, ] - async def async_state_change_event(self, _event: Event) -> None: - if self._listeners: - self._schedule_refresh() - - self.async_update_listeners() - async def async_set_control_setpoint(self, value: float) -> None: await self._publish_command(f'{{"cmd": "selflowtemp", "value": {0 if value == 10 else value}}}') await super().async_set_control_setpoint(value) diff --git a/custom_components/sat/util.py b/custom_components/sat/util.py index bcd8fe62..714212a1 100644 --- a/custom_components/sat/util.py +++ b/custom_components/sat/util.py @@ -98,16 +98,16 @@ def create_minimum_setpoint_controller(config_data, config_options) -> MinimumSe return MinimumSetpoint(configured_minimum_setpoint=minimum_setpoint, adjustment_factor=adjustment_factor) -def snake_case(value: str): +def snake_case(value: str) -> str: return '_'.join( sub('([A-Z][a-z]+)', r' \1', sub('([A-Z]+)', r' \1', value.replace('-', ' '))).split()).lower() -def float_value(value) -> float: +def float_value(value) -> float | None: """Helper method to convert a value to float, handling possible errors.""" try: return float(value) except (TypeError, ValueError): - return 0 + return None From 4bee7295d0ba9a726cc158af4115d57bff4ea278 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 7 Dec 2024 17:50:57 +0100 Subject: [PATCH 132/213] Add valves to the list of supported entities for the Switch Coordinator --- custom_components/sat/config_flow.py | 3 ++- custom_components/sat/manifest.json | 2 +- hacs.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index c60ba023..335fb3f2 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, ATTR_ENTITY_ID @@ -213,7 +214,7 @@ async def async_step_switch(self, _user_input: dict[str, Any] | None = None): data_schema=vol.Schema({ vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_DEVICE): selector.EntitySelector( - selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]) + selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN, VALVE_DOMAIN, INPUT_BOOLEAN_DOMAIN]) ), vol.Required(CONF_MINIMUM_SETPOINT, default=50): selector.NumberSelector( selector.NumberSelectorConfig(min=10, max=100, step=1) diff --git a/custom_components/sat/manifest.json b/custom_components/sat/manifest.json index c2c7cfa2..0e9e895b 100644 --- a/custom_components/sat/manifest.json +++ b/custom_components/sat/manifest.json @@ -24,5 +24,5 @@ "requirements": [ "pyotgw==2.1.3" ], - "version": "3.1.0" + "version": "4.0.0-alpha" } \ No newline at end of file diff --git a/hacs.json b/hacs.json index 3d319078..fde4a140 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "hacs": "1.6.0", "render_readme": true, - "homeassistant": "2023.1.0", + "homeassistant": "2024.1.0", "name": "Smart Autotune Thermostat" } From 90a97e6ddd1b5befb7e1ec18693393be6531980c Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 7 Dec 2024 17:51:33 +0100 Subject: [PATCH 133/213] Bump the HA requirement to 2024.1 --- custom_components/sat/config_flow.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 335fb3f2..c13ab66f 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback from homeassistant.helpers import selector, device_registry, entity_registry from homeassistant.helpers.selector import SelectSelectorMode @@ -64,10 +64,7 @@ async def async_step_user(self, _user_input: dict[str, Any] | None = None): """Handle user flow.""" menu_options = [] - # Since we rely on the availability logic in 2023.5, we do not support below it. - if MAJOR_VERSION >= 2023 and (MINOR_VERSION >= 5 or MAJOR_VERSION > 2023): - menu_options.append("mosquitto") - + menu_options.append("mosquitto") menu_options.append("esphome") menu_options.append("serial") menu_options.append("switch") From a89cdb6a4098dcdb67b722d7aaa9c8f1b1d13c06 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 7 Dec 2024 18:57:00 +0100 Subject: [PATCH 134/213] Improved config flow for MQTT --- custom_components/sat/config_flow.py | 78 ++++++++++++++-------- custom_components/sat/const.py | 2 - custom_components/sat/translations/de.json | 33 ++++++--- custom_components/sat/translations/en.json | 34 +++++++--- custom_components/sat/translations/es.json | 33 ++++++--- custom_components/sat/translations/fr.json | 33 ++++++--- custom_components/sat/translations/it.json | 33 ++++++--- custom_components/sat/translations/nl.json | 35 +++++++--- custom_components/sat/translations/pt.json | 36 +++++++--- custom_components/sat/translations/sk.json | 38 +++++++---- 10 files changed, 247 insertions(+), 108 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index c13ab66f..e977c984 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -5,7 +5,6 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import mqtt from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ATTR_HVAC_MODE, HVACMode, SERVICE_SET_HVAC_MODE from homeassistant.components.dhcp import DhcpServiceInfo @@ -62,12 +61,12 @@ def async_remove(self) -> None: async def async_step_user(self, _user_input: dict[str, Any] | None = None): """Handle user flow.""" - menu_options = [] - - menu_options.append("mosquitto") - menu_options.append("esphome") - menu_options.append("serial") - menu_options.append("switch") + menu_options = [ + "mosquitto", + "esphome", + "serial", + "switch" + ] if self.show_advanced_options: menu_options.append("simulator") @@ -114,40 +113,65 @@ async def async_step_mqtt(self, discovery_info: MqttServiceInfo): return await self.async_step_mosquitto() async def async_step_mosquitto(self, _user_input: dict[str, Any] | None = None): + """Entry step to select the MQTT mode and branch to specific setup.""" self.errors = {} if _user_input is not None: self.data.update(_user_input) - if self.data[CONF_TYPE] == MODE_MQTT_OPENTHERM: - self.data[CONF_MODE] = MODE_MQTT_OPENTHERM - - if self.data[CONF_TYPE] == MODE_MQTT_EMS: - self.data[CONF_MODE] = MODE_MQTT_EMS - - # Since we do not require this to be stored - del self.data[CONF_TYPE] - - if not await mqtt.async_wait_for_mqtt_client(self.hass): - self.errors["base"] = "mqtt_component" - return await self.async_step_mosquitto() + if self.data[CONF_MODE] == MODE_MQTT_OPENTHERM: + return await self.async_step_mosquitto_opentherm() - return await self.async_step_sensors() + if self.data[CONF_MODE] == MODE_MQTT_EMS: + return await self.async_step_mosquitto_ems() return self.async_show_form( step_id="mosquitto", last_step=False, errors=self.errors, data_schema=vol.Schema({ - vol.Required(CONF_TYPE): selector.SelectSelector( - selector.SelectSelectorConfig(mode=SelectSelectorMode.DROPDOWN, options=[ - selector.SelectOptionDict(value=MODE_MQTT_OPENTHERM, label="OpenTherm Gateway"), - selector.SelectOptionDict(value=MODE_MQTT_EMS, label="EMS-ESP"), - ]) + vol.Required(CONF_MODE): selector.SelectSelector( + selector.SelectSelectorConfig( + mode=SelectSelectorMode.DROPDOWN, + options=[ + selector.SelectOptionDict(value=MODE_MQTT_OPENTHERM, label="OpenTherm Gateway (For advanced boiler control)"), + selector.SelectOptionDict(value=MODE_MQTT_EMS, label="EMS-ESP (For Bosch, Junkers, Buderus systems)"), + ] + ) ), vol.Required(CONF_NAME, default=DEFAULT_NAME): str, - vol.Required(CONF_MQTT_TOPIC, default=OPTIONS_DEFAULTS[CONF_MQTT_TOPIC]): str, - vol.Required(CONF_DEVICE, default=self.data.get(CONF_DEVICE, "otgw-XXXXXXXXXXXX")): str, + }), + ) + + async def async_step_mosquitto_opentherm(self, _user_input: dict[str, Any] | None = None): + """Setup specific to OpenTherm Gateway.""" + if _user_input is not None: + self.data.update(_user_input) + + return await self.async_step_sensors() + + return self.async_show_form( + step_id="mosquitto_opentherm", + last_step=False, + data_schema=vol.Schema({ + vol.Required(CONF_MQTT_TOPIC, default="OTGW"): str, + vol.Required(CONF_DEVICE, default="otgw-XXXXXXXXXXXX"): str, + }), + ) + + async def async_step_mosquitto_ems(self, _user_input: dict[str, Any] | None = None): + """Setup specific to EMS-ESP.""" + if _user_input is not None: + self.data.update(_user_input) + self.data[CONF_DEVICE] = "ems-esp" + + return await self.async_step_sensors() + + return self.async_show_form( + step_id="mosquitto_ems", + last_step=False, + data_schema=vol.Schema({ + vol.Required(CONF_MQTT_TOPIC, default="ems-esp"): str, }), ) diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index f6bd4d56..7fec5fb5 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -27,7 +27,6 @@ # Configuration and options CONF_MODE = "mode" CONF_NAME = "name" -CONF_TYPE = "type" CONF_DEVICE = "device" CONF_CYCLES_PER_HOUR = "cycles_per_hour" CONF_SIMULATED_HEATING = "simulated_heating" @@ -125,7 +124,6 @@ CONF_MINIMUM_CONSUMPTION: 0, CONF_MAXIMUM_CONSUMPTION: 0, - CONF_MQTT_TOPIC: "OTGW", CONF_DUTY_CYCLE: "00:13:00", CONF_SAMPLE_TIME: "00:01:00", CONF_CLIMATE_VALVE_OFFSET: 0, diff --git a/custom_components/sat/translations/de.json b/custom_components/sat/translations/de.json index ca35a859..312a8fdd 100644 --- a/custom_components/sat/translations/de.json +++ b/custom_components/sat/translations/de.json @@ -53,13 +53,27 @@ "title": "Heizsystem" }, "mosquitto": { + "description": "Konfigurieren Sie den MQTT-Modus für Ihr Heizsystem. Wählen Sie den Gateway-Modus aus und geben Sie seinen Namen an.", "data": { - "device": "Gerät", - "mqtt_topic": "Top-Thema", + "mode": "Modus", "name": "Name" }, - "description": "Bitte geben Sie die folgenden Details an, um das OpenTherm Gateway einzurichten. Geben Sie im Feld Name einen Namen für das Gateway ein, der Ihnen hilft, es in Ihrem System zu identifizieren.\n\nGeben Sie die Klimaentität an, die für das OpenTherm Gateway verwendet wird. Diese Entität wird vom OpenTherm Gateway bereitgestellt und repräsentiert Ihr Heizsystem.\n\nGeben Sie außerdem das Top-Thema an, das für das Veröffentlichen und Abonnieren von MQTT-Nachrichten im Zusammenhang mit dem OpenTherm Gateway verwendet wird.\n\nDiese Einstellungen sind wesentlich, um die Kommunikation und Integration mit Ihrem OpenTherm Gateway über MQTT herzustellen. Sie ermöglichen einen nahtlosen Datenaustausch und die Steuerung Ihres Heizsystems. Stellen Sie sicher, dass die angegebenen Details korrekt sind, um eine ordnungsgemäße Funktionalität zu gewährleisten.", - "title": "OpenTherm Gateway ( MQTT )" + "title": "MQTT-Konfiguration" + }, + "mosquitto_opentherm": { + "description": "Richten Sie das OpenTherm Gateway ein. Geben Sie das Haupt-MQTT-Thema und die Gerätekennung an.", + "data": { + "mqtt_topic": "MQTT-Thema", + "device": "Geräte-ID" + }, + "title": "OpenTherm Gateway Einrichtung" + }, + "mosquitto_ems": { + "description": "Richten Sie das EMS-ESP Gateway ein. Geben Sie das MQTT-Thema an.", + "data": { + "mqtt_topic": "MQTT-Thema" + }, + "title": "EMS-ESP Gateway Einrichtung" }, "overshoot_protection": { "data": { @@ -116,12 +130,13 @@ "title": "PID-Thermostat mit PWM ( EIN/AUS )" }, "user": { - "description": "SAT ist ein intelligentes Thermostat, das in der Lage ist, sich selbst automatisch zu justieren, um die Temperaturregelung zu optimieren. Wählen Sie den passenden Modus, der zu Ihrem Heizsystem passt.", + "description": "SAT ist ein intelligentes Thermostat, das sich selbst optimieren kann, um die Temperaturregelung zu verbessern. Wählen Sie den Modus, der zu Ihrem Heizsystem passt.", "menu_options": { - "mosquitto": "OpenTherm Gateway ( MQTT )", - "serial": "OpenTherm Gateway ( SERIELL )", - "simulator": "Simuliertes Gateway ( FORTGESCHRITTEN )", - "switch": "PID-Thermostat mit PWM ( EIN/AUS )" + "mosquitto": "MQTT-Gateway (OpenTherm, EMS-ESP, andere)", + "serial": "Serielles Gateway (z. B. OpenTherm)", + "esphome": "ESPHome (Systemdienste und Sensoren)", + "simulator": "Simuliertes Gateway (nur für Entwickler)", + "switch": "PID-Thermostat (PWM Ein/Aus-Modus)" }, "title": "Smart Autotune Thermostat (SAT)" } diff --git a/custom_components/sat/translations/en.json b/custom_components/sat/translations/en.json index a8e0c915..f776a24c 100644 --- a/custom_components/sat/translations/en.json +++ b/custom_components/sat/translations/en.json @@ -53,13 +53,27 @@ "title": "Heating System" }, "mosquitto": { + "description": "Configure the MQTT mode for your heating system. Select the gateway mode and provide its name.", "data": { - "device": "Device", - "mqtt_topic": "Top Topic", + "mode": "Mode", "name": "Name" }, - "description": "Please provide the following details to set up the OpenTherm Gateway. In the Name field, enter a name for the gateway that will help you identify it within your system.\n\nSpecify the Climate entity to use for the OpenTherm Gateway. This entity is provided by the OpenTherm Gateway and represents your heating system.\n\nAdditionally, enter the Top Topic that will be used for publishing and subscribing to MQTT messages related to the OpenTherm Gateway.\n\nThese settings are essential for establishing communication and integration with your OpenTherm Gateway through MQTT. They allow for seamless data exchange and control of your heating system. Ensure that the provided details are accurate to ensure proper functionality.", - "title": "OpenTherm Gateway ( MQTT )" + "title": "MQTT Configuration" + }, + "mosquitto_opentherm": { + "description": "Set up the OpenTherm Gateway. Provide the top MQTT topic and the device identifier.", + "data": { + "mqtt_topic": "MQTT Topic", + "device": "Device ID" + }, + "title": "OpenTherm Gateway Setup" + }, + "mosquitto_ems": { + "description": "Set up the EMS-ESP Gateway. Provide the MQTT topic.", + "data": { + "mqtt_topic": "MQTT Topic" + }, + "title": "EMS-ESP Gateway Setup" }, "overshoot_protection": { "data": { @@ -116,13 +130,13 @@ "title": "PID Thermostat with PWM ( ON/OFF )" }, "user": { - "description": "SAT is a smart thermostat that is capable of auto-tuning itself to optimize temperature control. Select the appropriate mode that matches your heating system.", + "description": "SAT is a smart thermostat capable of auto-tuning itself to optimize temperature control. Select the mode that matches your heating system.", "menu_options": { - "mosquitto": "OpenTherm Gateway ( MQTT )", - "serial": "OpenTherm Gateway ( SERIAL )", - "esphome": "OpenTherm Gateway ( ESPHOME )", - "simulator": "Simulated Gateway ( ADVANCED )", - "switch": "PID Thermostat with PWM ( ON/OFF )" + "mosquitto": "MQTT Gateway (OpenTherm, EMS-ESP, others)", + "serial": "Serial Gateway (e.g., OpenTherm)", + "esphome": "ESPHome (System services and sensors)", + "simulator": "Simulated Gateway (Developer use only)", + "switch": "PID Thermostat (PWM On/Off mode)" }, "title": "Smart Autotune Thermostat (SAT)" } diff --git a/custom_components/sat/translations/es.json b/custom_components/sat/translations/es.json index aac024a3..658be724 100644 --- a/custom_components/sat/translations/es.json +++ b/custom_components/sat/translations/es.json @@ -53,13 +53,27 @@ "title": "Sistema de Calefacción" }, "mosquitto": { + "description": "Configure el modo MQTT para su sistema de calefacción. Seleccione el modo de la puerta de enlace e introduzca su nombre.", "data": { - "device": "Dispositivo", - "mqtt_topic": "Tema Principal", + "mode": "Modo", "name": "Nombre" }, - "description": "Proporcione los siguientes detalles para configurar la Puerta de Enlace OpenTherm. En el campo Nombre, introduzca un nombre para la puerta de enlace que le ayude a identificarla dentro de su sistema.\n\nEspecifique la entidad Climática que usará para la Puerta de Enlace OpenTherm. Esta entidad es proporcionada por la Puerta de Enlace OpenTherm y representa su sistema de calefacción.\n\nAdicionalmente, introduzca el Tema Principal que se usará para publicar y suscribirse a mensajes MQTT relacionados con la Puerta de Enlace OpenTherm.\n\nEstos ajustes son esenciales para establecer la comunicación e integración con su Puerta de Enlace OpenTherm a través de MQTT. Permiten un intercambio de datos sin problemas y el control de su sistema de calefacción. Asegúrese de que los detalles proporcionados sean precisos para garantizar una funcionalidad adecuada.", - "title": "Puerta de Enlace OpenTherm (MQTT)" + "title": "Configuración MQTT" + }, + "mosquitto_opentherm": { + "description": "Configure la Puerta de Enlace OpenTherm. Proporcione el tema principal de MQTT y el identificador del dispositivo.", + "data": { + "mqtt_topic": "Tema MQTT", + "device": "ID del dispositivo" + }, + "title": "Configuración de la Puerta de Enlace OpenTherm" + }, + "mosquitto_ems": { + "description": "Configure la Puerta de Enlace EMS-ESP. Proporcione el tema MQTT.", + "data": { + "mqtt_topic": "Tema MQTT" + }, + "title": "Configuración de la Puerta de Enlace EMS-ESP" }, "overshoot_protection": { "data": { @@ -116,12 +130,13 @@ "title": "Termostato PID con PWM (ON/OFF)" }, "user": { - "description": "SAT es un termostato inteligente capaz de autoajustarse para optimizar el control de la temperatura. Seleccione el modo apropiado que coincida con su sistema de calefacción.", + "description": "SAT es un termostato inteligente capaz de autoajustarse para optimizar el control de temperatura. Seleccione el modo que se ajuste a su sistema de calefacción.", "menu_options": { - "mosquitto": "Puerta de Enlace OpenTherm (MQTT)", - "serial": "Puerta de Enlace OpenTherm (SERIAL)", - "simulator": "Puerta de Enlace Simulada (AVANZADO)", - "switch": "Termostato PID con PWM (ON/OFF)" + "mosquitto": "Puerta de enlace MQTT (OpenTherm, EMS-ESP, otros)", + "serial": "Puerta de enlace serial (p. ej., OpenTherm)", + "esphome": "ESPHome (Servicios y sensores del sistema)", + "simulator": "Puerta de enlace simulada (Solo para desarrolladores)", + "switch": "Termostato PID (Modo PWM Encendido/Apagado)" }, "title": "Smart Autotune Thermostat (SAT)" } diff --git a/custom_components/sat/translations/fr.json b/custom_components/sat/translations/fr.json index 1918e566..02cc2e82 100644 --- a/custom_components/sat/translations/fr.json +++ b/custom_components/sat/translations/fr.json @@ -53,13 +53,27 @@ "title": "Système de chauffage" }, "mosquitto": { + "description": "Configurez le mode MQTT pour votre système de chauffage. Sélectionnez le mode de la passerelle et fournissez son nom.", "data": { - "device": "Appareil", - "mqtt_topic": "Sujet Principal", + "mode": "Mode", "name": "Nom" }, - "description": "Veuillez fournir les détails suivants pour configurer la passerelle OpenTherm. Dans le champ Nom, entrez un nom pour la passerelle qui vous aidera à l'identifier au sein de votre système.\n\nSpécifiez l'entité Climat à utiliser pour la passerelle OpenTherm. Cette entité est fournie par la passerelle OpenTherm et représente votre système de chauffage.\n\nDe plus, entrez le Sujet principal qui sera utilisé pour publier et s'abonner aux messages MQTT liés à la passerelle OpenTherm.\n\nCes paramètres sont essentiels pour établir la communication et l'intégration avec votre passerelle OpenTherm via MQTT. Ils permettent un échange de données et un contrôle fluides de votre système de chauffage. Assurez-vous que les détails fournis sont précis pour garantir une fonctionnalité appropriée.", - "title": "OpenTherm Gateway ( MQTT )" + "title": "Configuration MQTT" + }, + "mosquitto_opentherm": { + "description": "Configurez la passerelle OpenTherm. Fournissez le sujet principal MQTT et l'identifiant de l'appareil.", + "data": { + "mqtt_topic": "Sujet MQTT", + "device": "ID de l'appareil" + }, + "title": "Configuration de la Passerelle OpenTherm" + }, + "mosquitto_ems": { + "description": "Configurez la passerelle EMS-ESP. Fournissez le sujet MQTT.", + "data": { + "mqtt_topic": "Sujet MQTT" + }, + "title": "Configuration de la Passerelle EMS-ESP" }, "overshoot_protection": { "data": { @@ -116,12 +130,13 @@ "title": "Thermostat PID avec PWM (ON/OFF)" }, "user": { - "description": "Le SAT est un thermostat intelligent capable de s'auto-ajuster pour optimiser le contrôle de la température. Sélectionnez le mode approprié qui correspond à votre système de chauffage.", + "description": "SAT est un thermostat intelligent capable de s'auto-ajuster pour optimiser le contrôle de la température. Sélectionnez le mode correspondant à votre système de chauffage.", "menu_options": { - "mosquitto": "OpenTherm Gateway ( MQTT )", - "serial": "OpenTherm Gateway ( SERIAL )", - "simulator": "Passerelle simulée ( AVANCÉ )", - "switch": "Thermostat PID avec PWM ( ON/OFF )" + "mosquitto": "Passerelle MQTT (OpenTherm, EMS-ESP, autres)", + "serial": "Passerelle série (ex. : OpenTherm)", + "esphome": "ESPHome (Services et capteurs système)", + "simulator": "Passerelle simulée (Réservé aux développeurs)", + "switch": "Thermostat PID (Mode PWM Marche/Arrêt)" }, "title": "Smart Autotune Thermostat (SAT)" } diff --git a/custom_components/sat/translations/it.json b/custom_components/sat/translations/it.json index b9b15f10..5e88da20 100644 --- a/custom_components/sat/translations/it.json +++ b/custom_components/sat/translations/it.json @@ -53,13 +53,27 @@ "title": "Sistema di Riscaldamento" }, "mosquitto": { + "description": "Configura la modalità MQTT per il tuo sistema di riscaldamento. Seleziona la modalità del gateway e fornisci il suo nome.", "data": { - "device": "Dispositivo", - "mqtt_topic": "Argomento Principale", + "mode": "Modalità", "name": "Nome" }, - "description": "Si prega di fornire i seguenti dettagli per configurare il Gateway OpenTherm. Nel campo Nome, inserisci un nome per il gateway che ti aiuterà a identificarlo all'interno del tuo sistema.\n\nSpecifica l'entità Climatica da utilizzare per il Gateway OpenTherm. Questa entità è fornita dal Gateway OpenTherm e rappresenta il tuo sistema di riscaldamento.\n\nInoltre, inserisci l'Argomento Principale che verrà utilizzato per pubblicare e sottoscrivere i messaggi MQTT relativi al Gateway OpenTherm.\n\nQueste impostazioni sono essenziali per stabilire la comunicazione e l'integrazione con il tuo Gateway OpenTherm tramite MQTT. Consentono uno scambio di dati e un controllo fluido del tuo sistema di riscaldamento. Assicurati che i dettagli forniti siano accurati per garantire un corretto funzionamento.", - "title": "Gateway OpenTherm (MQTT)" + "title": "Configurazione MQTT" + }, + "mosquitto_opentherm": { + "description": "Configura il Gateway OpenTherm. Fornisci l'argomento principale MQTT e l'identificativo del dispositivo.", + "data": { + "mqtt_topic": "Argomento MQTT", + "device": "ID del dispositivo" + }, + "title": "Configurazione del Gateway OpenTherm" + }, + "mosquitto_ems": { + "description": "Configura il Gateway EMS-ESP. Fornisci l'argomento MQTT.", + "data": { + "mqtt_topic": "Argomento MQTT" + }, + "title": "Configurazione del Gateway EMS-ESP" }, "overshoot_protection": { "data": { @@ -116,12 +130,13 @@ "title": "Termostato PID con PWM (ON/OFF)" }, "user": { - "description": "Il SAT è un termostato intelligente capace di auto-regolarsi per ottimizzare il controllo della temperatura. Seleziona la modalità appropriata che corrisponde al tuo sistema di riscaldamento.", + "description": "SAT è un termostato intelligente in grado di auto-regolarsi per ottimizzare il controllo della temperatura. Seleziona la modalità che corrisponde al tuo sistema di riscaldamento.", "menu_options": { - "mosquitto": "Gateway OpenTherm (MQTT)", - "serial": "Gateway OpenTherm (SERIALE)", - "simulator": "Gateway Simulato (AVANZATO)", - "switch": "Termostato PID con PWM (ON/OFF)" + "mosquitto": "Gateway MQTT (OpenTherm, EMS-ESP, altri)", + "serial": "Gateway seriale (es. OpenTherm)", + "esphome": "ESPHome (Servizi e sensori di sistema)", + "simulator": "Gateway simulato (Solo per sviluppatori)", + "switch": "Termostato PID (Modalità PWM Acceso/Spento)" }, "title": "Smart Autotune Thermostat (SAT)" } diff --git a/custom_components/sat/translations/nl.json b/custom_components/sat/translations/nl.json index aeaa72c9..e7f43156 100644 --- a/custom_components/sat/translations/nl.json +++ b/custom_components/sat/translations/nl.json @@ -53,13 +53,27 @@ "title": "Verwarmingssysteem" }, "mosquitto": { + "description": "Configureer de MQTT-modus voor uw verwarmingssysteem. Kies de gateway-modus en geef de naam op.", "data": { - "device": "Apparaat", - "mqtt_topic": "Top Topic", + "mode": "Modus", "name": "Naam" }, - "description": "Gelieve de volgende gegevens te verstrekken om de OpenTherm Gateway in te stellen. Voer in het veld Naam een naam in voor de gateway die u helpt deze te identificeren binnen uw systeem.\n\nSpecificeer de Climate entity die gebruikt wordt voor de OpenTherm Gateway. Deze entiteit wordt aangeleverd door de OpenTherm Gateway en vertegenwoordigt uw verwarmingssysteem.\n\nVoer daarnaast het Top Topic in dat gebruikt zal worden voor het publiceren en abonneren op MQTT-berichten gerelateerd aan de OpenTherm Gateway.\n\nDeze instellingen zijn essentieel voor het tot stand brengen van communicatie en integratie met uw OpenTherm Gateway via MQTT. Ze zorgen voor een naadloze gegevensuitwisseling en controle over uw verwarmingssysteem. Zorg ervoor dat de verstrekte gegevens nauwkeurig zijn om een correcte werking te garanderen.", - "title": "OpenTherm Gateway ( MQTT )" + "title": "MQTT-configuratie" + }, + "mosquitto_opentherm": { + "description": "Stel de OpenTherm Gateway in. Geef het belangrijkste MQTT-onderwerp en het apparaat-ID op.", + "data": { + "mqtt_topic": "MQTT-onderwerp", + "device": "Apparaat-ID" + }, + "title": "OpenTherm Gateway Configuratie" + }, + "mosquitto_ems": { + "description": "Stel de EMS-ESP Gateway in. Geef het MQTT-onderwerp op.", + "data": { + "mqtt_topic": "MQTT-onderwerp" + }, + "title": "EMS-ESP Gateway Configuratie" }, "overshoot_protection": { "data": { @@ -116,14 +130,15 @@ "title": "PID Thermostaat met PWM ( AAN/UIT )" }, "user": { - "description": "SAT is een slimme thermostaat die zichzelf kan afstemmen om de temperatuurregeling te optimaliseren. Selecteer de geschikte modus die overeenkomt met uw verwarmingssysteem.", + "description": "SAT is een slimme thermostaat die zichzelf kan optimaliseren om de temperatuurregeling te verbeteren. Kies de modus die past bij uw verwarmingssysteem.", "menu_options": { - "mosquitto": "OpenTherm Gateway ( MQTT )", - "serial": "OpenTherm Gateway ( SERIEEL )", - "simulator": "Gesimuleerde Gateway ( GEAVANCEERD )", - "switch": "PID Thermostaat met PWM ( AAN/UIT )" + "mosquitto": "MQTT Gateway (OpenTherm, EMS-ESP, andere)", + "serial": "Seriële Gateway (bijv. OpenTherm)", + "esphome": "ESPHome (Systeemservices en sensoren)", + "simulator": "Gesimuleerde Gateway (Alleen voor ontwikkelaars)", + "switch": "PID-Thermostaat (PWM Aan/Uit-modus)" }, - "title": "Slimme Autotune Thermostaat (SAT)" + "title": "Smart Autotune Thermostat (SAT)" } } }, diff --git a/custom_components/sat/translations/pt.json b/custom_components/sat/translations/pt.json index cbe42414..6bc85761 100644 --- a/custom_components/sat/translations/pt.json +++ b/custom_components/sat/translations/pt.json @@ -53,13 +53,27 @@ "title": "Sistema de Aquecimento" }, "mosquitto": { + "description": "Configure o modo MQTT para o seu sistema de aquecimento. Escolha o modo de gateway e forneça seu nome.", "data": { - "device": "Dispositivo", - "mqtt_topic": "Tópico Principal", + "mode": "Modo", "name": "Nome" }, - "description": "Por favor forneça os seguintes detalhes para configurar o OpenTherm Gateway. No campo Nome, insira um nome para o gateway que o ajudará a identificá-lo dentro do seu sistema.\n\nEspecifique a entidade Climate a utilizar para o OpenTherm Gateway. Esta entidade é fornecida pelo OpenTherm Gateway e representa o seu sistema de aquecimento.\n\nAdicionalmente, insira o Tópico Principal que será usado para publicar e subscrever mensagens MQTT relacionadas com o OpenTherm Gateway.\n\nEstas definições são essenciais para estabelecer comunicação e integração com o seu OpenTherm Gateway através do MQTT. Elas permitem troca de dados e controlo do seu sistema de aquecimento de forma fluida. Assegure-se de que os detalhes fornecidos são precisos para garantir funcionalidade adequada.", - "title": "OpenTherm Gateway ( MQTT )" + "title": "Configuração MQTT" + }, + "mosquitto_opentherm": { + "description": "Configure o Gateway OpenTherm. Forneça o tópico MQTT principal e o identificador do dispositivo.", + "data": { + "mqtt_topic": "Tópico MQTT", + "device": "ID do dispositivo" + }, + "title": "Configuração do Gateway OpenTherm" + }, + "mosquitto_ems": { + "description": "Configure o Gateway EMS-ESP. Forneça o tópico MQTT.", + "data": { + "mqtt_topic": "Tópico MQTT" + }, + "title": "Configuração do Gateway EMS-ESP" }, "overshoot_protection": { "data": { @@ -116,15 +130,15 @@ "title": "Termóstato PID com PWM ( LIGA/DESLIGA )" }, "user": { - "description": "O SAT é um termóstato inteligente capaz de auto-afinar-se para otimizar o controlo de temperatura. Selecione o modo apropriado que corresponde ao seu sistema de aquecimento.", + "description": "SAT é um termostato inteligente capaz de autoajustar-se para otimizar o controle de temperatura. Escolha o modo que corresponde ao seu sistema de aquecimento.", "menu_options": { - "mosquitto": "OpenTherm Gateway ( MQTT )", - "serial": "OpenTherm Gateway ( SERIAL )", - "esphome": "OpenTherm Gateway ( ESPHOME )", - "simulator": "Gateway Simulado ( AVANÇADO )", - "switch": "Termóstato PID com PWM ( LIGA/DESLIGA )" + "mosquitto": "Gateway MQTT (OpenTherm, EMS-ESP, outros)", + "serial": "Gateway Serial (ex.: OpenTherm)", + "esphome": "ESPHome (Serviços e sensores do sistema)", + "simulator": "Gateway Simulado (Apenas para desenvolvedores)", + "switch": "Termostato PID (Modo PWM Ligado/Desligado)" }, - "title": "Termóstato Smart Autotune (SAT)" + "title": "Smart Autotune Thermostat (SAT)" } } }, diff --git a/custom_components/sat/translations/sk.json b/custom_components/sat/translations/sk.json index ec8d4f14..ac890329 100644 --- a/custom_components/sat/translations/sk.json +++ b/custom_components/sat/translations/sk.json @@ -53,13 +53,27 @@ "title": "Vykurovací systém" }, "mosquitto": { + "description": "Nakonfigurujte režim MQTT pre váš vykurovací systém. Vyberte režim brány a zadajte jej názov.", "data": { - "device": "Zariadenia", - "mqtt_topic": "Hlavná téma", - "name": "Meno" + "mode": "Režim", + "name": "Názov" + }, + "title": "Konfigurácia MQTT" + }, + "mosquitto_opentherm": { + "description": "Nakonfigurujte OpenTherm Gateway. Uveďte hlavný predmet MQTT a identifikátor zariadenia.", + "data": { + "mqtt_topic": "Predmet MQTT", + "device": "ID zariadenia" + }, + "title": "Konfigurácia OpenTherm Gateway" + }, + "mosquitto_ems": { + "description": "Nakonfigurujte EMS-ESP Gateway. Uveďte predmet MQTT.", + "data": { + "mqtt_topic": "Predmet MQTT" }, - "description": "Ak chcete nastaviť bránu OpenTherm, uveďte nasledujúce podrobnosti. Do poľa Názov zadajte názov brány, ktorý vám pomôže identifikovať ju vo vašom systéme.\n\nZadajte entitu Climate, ktorá sa má použiť pre bránu OpenTherm. Túto entitu poskytuje brána OpenTherm a predstavuje váš vykurovací systém.\n\nOkrem toho zadajte hlavnú tému, ktorá sa použije na publikovanie a prihlásenie na odber správ MQTT súvisiacich s bránou OpenTherm.\n\nTieto nastavenia sú nevyhnutné na nadviazanie komunikácie a integráciu s vašou bránou OpenTherm cez MQTT. Umožňujú bezproblémovú výmenu dát a ovládanie vášho vykurovacieho systému. Uistite sa, že poskytnuté podrobnosti sú presné, aby sa zabezpečila správna funkčnosť.", - "title": "OpenTherm Gateway ( MQTT )" + "title": "Konfigurácia EMS-ESP Gateway" }, "overshoot_protection": { "data": { @@ -116,13 +130,13 @@ "title": "PID termostat s PWM (ON/OFF)" }, "user": { - "description": "SAT je inteligentný termostat, ktorý je schopný samočinného ladenia na optimalizáciu regulácie teploty. Vyberte vhodný režim, ktorý zodpovedá vášmu vykurovaciemu systému.", + "description": "SAT je inteligentný termostat schopný samonastavenia na optimalizáciu regulácie teploty. Vyberte režim, ktorý zodpovedá vášmu vykurovaciemu systému.", "menu_options": { - "mosquitto": "OpenTherm Gateway ( MQTT )", - "serial": "OpenTherm Gateway ( SERIAL )", - "esphome": "OpenTherm Gateway ( ESPHOME )", - "simulator": "Simulated Gateway ( POKROČILÉ )", - "switch": "PID Thermostat with PWM ( ON/OFF )" + "mosquitto": "MQTT Gateway (OpenTherm, EMS-ESP, iné)", + "serial": "Sériová brána (napr. OpenTherm)", + "esphome": "ESPHome (Systémové služby a senzory)", + "simulator": "Simulovaná brána (Len pre vývojárov)", + "switch": "PID Termostat (PWM režim Zap/Vyp)" }, "title": "Smart Autotune Thermostat (SAT)" } @@ -175,7 +189,7 @@ "data_description": { "automatic_gains_value": "Hodnota používaná pre automatické zosilnenie v PID regulátore.", "derivative": "Odvodený člen (kD) v PID regulátore, zodpovedný za zmiernenie prekročenia.", - "derivative_time_weight": "Parameter na úpravu vplyvu derivačného členu v priebehu času, obzvlášť užitočný na zníženie podkmitu počas zahrievacej fázy, keď je koeficient vykurovacej krivky správne nastavený.", + "derivative_time_weight": "Parameter na úpravu vplyvu derivačného členu v priebehu času, obzvlášť užitočný na zníženie podkmitu počas zahrievacej fázy, keď je koeficient vykurovacej krivky správne nastavený.", "duty_cycle": "Maximálny pracovný cyklus pre moduláciu šírky impulzu (PWM), ktorá riadi cykly zapnutia a vypnutia kotla.", "heating_curve_coefficient": "Koeficient použitý na úpravu vykurovacej krivky.", "integral": "Integrálny člen (kI) v regulátore PID, zodpovedný za zníženie chyby v ustálenom stave.", From b7f33084b0cecd347b468166245b029874fd56be Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 7 Dec 2024 19:06:50 +0100 Subject: [PATCH 135/213] Add support for parsing the "boiler_data" for EMS --- custom_components/sat/mqtt/__init__.py | 19 ++++++++++++------- custom_components/sat/mqtt/ems.py | 21 +++++++++------------ 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 97bd0464..786810da 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -90,22 +90,27 @@ async def boot(self) -> None: pass def _create_message_handler(self, key: str): - """Create a message handler to properly schedule updates.""" + """Create a message handler to process incoming MQTT messages.""" @callback - def message_handler(msg): - """Handle received MQTT message and schedule data update.""" - _LOGGER.debug(f"Receiving '{key}'='{msg.payload}' from MQTT.") + def message_handler(message): + """Handle an incoming MQTT message and schedule an update.""" + _LOGGER.debug("Received MQTT message for key '%s': payload='%s'", key, message.payload) - # Store the new value - self.data[key] = msg.payload + # Process the payload and update the data property + self._process_message_payload(key, message.payload) - # Schedule the update so our entities are updated + # Notify listeners to ensure the entities are updated self.hass.async_create_task(self.async_notify_listeners()) return message_handler + def _process_message_payload(self, key: str, payload): + """Process and store the payload of a received MQTT message.""" + self.data[key] = payload + async def _publish_command(self, payload: str): + """Publish a command to the MQTT topic.""" _LOGGER.debug(f"Publishing '{payload}' to MQTT.") if not self._simulation: diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index 1f19df78..c422a5d4 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -1,11 +1,13 @@ from __future__ import annotations +import json import logging from . import SatMqttCoordinator from ..coordinator import DeviceState from ..util import float_value +DATA_BOILER_DATA = "boiler_data" DATA_FLAME_ACTIVE = "burngas" DATA_DHW_SETPOINT = "dhw/seltemp" DATA_CONTROL_SETPOINT = "selflowtemp" @@ -96,18 +98,7 @@ async def boot(self) -> SatMqttCoordinator: return self def get_tracked_entities(self) -> list[str]: - return [ - DATA_CENTRAL_HEATING, - DATA_FLAME_ACTIVE, - DATA_DHW_ENABLE, - DATA_DHW_SETPOINT, - DATA_CONTROL_SETPOINT, - DATA_REL_MOD_LEVEL, - DATA_BOILER_TEMPERATURE, - DATA_BOILER_CAPACITY, - DATA_REL_MIN_MOD_LEVEL, - DATA_MAX_REL_MOD_LEVEL_SETTING, - ] + return [DATA_BOILER_DATA] async def async_set_control_setpoint(self, value: float) -> None: await self._publish_command(f'{{"cmd": "selflowtemp", "value": {0 if value == 10 else value}}}') @@ -142,3 +133,9 @@ def _get_topic_for_subscription(self, key: str) -> str: def _get_topic_for_publishing(self) -> str: return f"{self._topic}/boiler" + + def _process_message_payload(self, key: str, payload): + try: + self.data = json.loads(payload) + except json.JSONDecodeError as error: + _LOGGER.error("Failed to decode JSON payload: %s. Error: %s", payload, error) From 4072ee436276d69778a1fc67680fb9ceb168dba9 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 7 Dec 2024 19:08:19 +0100 Subject: [PATCH 136/213] Only use the "raw_derivative" --- custom_components/sat/pid.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/custom_components/sat/pid.py b/custom_components/sat/pid.py index dc4c4b11..c96a33ed 100644 --- a/custom_components/sat/pid.py +++ b/custom_components/sat/pid.py @@ -329,17 +329,7 @@ def integral(self) -> float: @property def derivative(self) -> float: """Return the derivative value.""" - derivative = self.kd * self._raw_derivative - output = self._last_heating_curve_value + self.proportional + self.integral - - if self._last_boiler_temperature is not None: - if abs(self._last_error) > 0.1 and abs(self._last_boiler_temperature - output) < 3: - return 0 - - if abs(self._last_error) <= 0.1 and abs(self._last_boiler_temperature - output) < 7: - return 0 - - return round(derivative, 3) + return round(self.kd * self._raw_derivative, 3) @property def raw_derivative(self) -> float: From ddca3f101d4e15223ec6a0ccbc0e9b8d6db74620 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 7 Dec 2024 19:18:02 +0100 Subject: [PATCH 137/213] No need for the device anymore --- custom_components/sat/config_flow.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index e977c984..93942712 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -9,7 +9,6 @@ from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ATTR_HVAC_MODE, HVACMode, SERVICE_SET_HVAC_MODE from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.input_boolean import DOMAIN as INPUT_BOOLEAN_DOMAIN -from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN @@ -17,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback -from homeassistant.helpers import selector, device_registry, entity_registry +from homeassistant.helpers import selector, entity_registry from homeassistant.helpers.selector import SelectSelectorMode from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from pyotgw import OpenThermGateway @@ -98,16 +97,12 @@ async def async_step_mqtt(self, discovery_info: MqttServiceInfo): device_id = "ems-esp" device_name = "EMS-ESP" - device = device_registry.async_get(self.hass).async_get_device( - {(MQTT_DOMAIN, device_id)} - ) - _LOGGER.debug("Discovered %s at [mqtt://%s]", device_name, discovery_info.topic) self.data[CONF_DEVICE] = device_id # abort if we already have exactly this gateway id/host # reload the integration if the host got updated - await self.async_set_unique_id(device.id) + await self.async_set_unique_id(device_id) self._abort_if_unique_id_configured(updates=self.data) return await self.async_step_mosquitto() From 888001652d5a3bd19cc6b5f1dd26aa9149bd409e Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 7 Dec 2024 19:24:30 +0100 Subject: [PATCH 138/213] Add some exception handling --- custom_components/sat/mqtt/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 786810da..384fef7c 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -97,8 +97,11 @@ def message_handler(message): """Handle an incoming MQTT message and schedule an update.""" _LOGGER.debug("Received MQTT message for key '%s': payload='%s'", key, message.payload) - # Process the payload and update the data property - self._process_message_payload(key, message.payload) + try: + # Process the payload and update the data property + self._process_message_payload(key, message.payload) + except Exception as e: + _LOGGER.error("Failed to process message for key '%s': %s", key, str(e)) # Notify listeners to ensure the entities are updated self.hass.async_create_task(self.async_notify_listeners()) @@ -114,4 +117,9 @@ async def _publish_command(self, payload: str): _LOGGER.debug(f"Publishing '{payload}' to MQTT.") if not self._simulation: + return + + try: await mqtt.async_publish(self.hass, self._get_topic_for_publishing(), payload) + except Exception as e: + _LOGGER.error("Failed to publish command: %s", str(e)) From 87dcb8765f6a526c10f74df6943e2f91b98ac6f0 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 7 Dec 2024 19:31:49 +0100 Subject: [PATCH 139/213] Only store the data when we shut down, so we don't spam the persistent storage --- custom_components/sat/mqtt/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 384fef7c..015b0461 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -48,15 +48,16 @@ async def async_added_to_hass(self, climate: SatClimate) -> None: await super().async_added_to_hass(climate) + async def async_will_remove_from_hass(self, climate: SatClimate) -> None: + # Save the updated data to persistent storage + await self._save_data() + async def async_notify_listeners(self): """Notify listeners of an update asynchronously.""" # Make sure we do not spam self._async_unsub_refresh() self._debounced_refresh.async_cancel() - # Save the updated data to persistent storage - await self._save_data() - # Inform the listeners that we are updated self.async_update_listeners() From 180075175039feefc429c20b1773579762e5d9f3 Mon Sep 17 00:00:00 2001 From: misa1515 <61636045+misa1515@users.noreply.github.com> Date: Sat, 7 Dec 2024 23:22:13 +0100 Subject: [PATCH 140/213] Update sk.json --- custom_components/sat/translations/sk.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/translations/sk.json b/custom_components/sat/translations/sk.json index ec8d4f14..a201de8f 100644 --- a/custom_components/sat/translations/sk.json +++ b/custom_components/sat/translations/sk.json @@ -191,7 +191,7 @@ "init": { "menu_options": { "advanced": "Rozšírené možnosti", - "general": "VŠeobecné", + "general": "Všeobecné", "presets": "Predvoľby", "system_configuration": "Konfigurácia systému" } @@ -203,6 +203,7 @@ "comfort_temperature": "Komfortná teplota", "home_temperature": "Domáca teplota", "sleep_temperature": "Teplota spánku", + "sync_with_thermostat": "Synchronizujte s termostatom pripojeným ku kotlu", "sync_climates_with_preset": "Synchronizujte klímu s predvoľbou (spánok / preč / aktivita)" }, "description": "Preddefinované nastavenia teploty pre rôzne scenáre alebo činnosti.", @@ -210,12 +211,15 @@ }, "system_configuration": { "data": { + "cycles_per_hour": "Pracovné cykly za hodinu", "automatic_duty_cycle": "Automatický pracovný cyklus", + "sync_climates_with_mode": "Synchronizujte klímu s režimom", "overshoot_protection": "Ochrana proti prekročeniu (s PWM)", "sensor_max_value_age": "Vek maximálnej hodnoty snímača teploty", "window_minimum_open_time": "Minimálny čas na otvorenie okna" }, "data_description": { + "cycles_per_hour": "Maximálny počet pracovných cyklov za hodinu.", "automatic_duty_cycle": "Povoliť alebo zakázať automatický pracovný cyklus pre moduláciu šírky impulzu (PWM).", "overshoot_protection": "Aktivujte ochranu proti prekročeniu pomocou modulácie šírky impulzov (PWM), aby ste zabránili prekročeniu teploty kotla.", "sensor_max_value_age": "Maximálny vek hodnoty teplotného snímača pred tým, než sa považuje za prerušenie.", From 512895c7460bff67ed8633b7ce6150d061a4891c Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 7 Dec 2024 23:25:31 +0100 Subject: [PATCH 141/213] Lower automatic gains and derivative-time weight step --- custom_components/sat/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 93942712..f3484400 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -560,10 +560,10 @@ async def async_step_general(self, _user_input: dict[str, Any] | None = None): if options[CONF_AUTOMATIC_GAINS]: schema[vol.Required(CONF_AUTOMATIC_GAINS_VALUE, default=options[CONF_AUTOMATIC_GAINS_VALUE])] = selector.NumberSelector( - selector.NumberSelectorConfig(min=1, max=5, step=1) + selector.NumberSelectorConfig(min=1, max=5, step=0.1) ) schema[vol.Required(CONF_DERIVATIVE_TIME_WEIGHT, default=options[CONF_DERIVATIVE_TIME_WEIGHT])] = selector.NumberSelector( - selector.NumberSelectorConfig(min=1, max=6, step=1) + selector.NumberSelectorConfig(min=1, max=6, step=0.1) ) else: schema[vol.Required(CONF_PROPORTIONAL, default=options[CONF_PROPORTIONAL])] = str From d7c2e26b26901f989ffb5139f6e7f2c0c916f7ff Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 00:16:07 +0100 Subject: [PATCH 142/213] Make use of the correct device name --- custom_components/sat/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index f3484400..3ad4d65c 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -375,8 +375,8 @@ async def async_step_calibrate(self, _user_input: dict[str, Any] | None = None): coordinator = await self.async_create_coordinator() # Let's see if we have already been configured before + device_name = self.data[CONF_NAME] entities = entity_registry.async_get(self.hass) - device_name = self.config_entry.data.get(CONF_NAME) climate_id = entities.async_get_entity_id(CLIMATE_DOMAIN, DOMAIN, device_name.lower()) async def start_calibration(): From 2fdd2ca6044614091b65ceed874ed76dcfd9e894 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 00:17:23 +0100 Subject: [PATCH 143/213] Make use of the correct device name --- custom_components/sat/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 1c22d5c5..afee28d2 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -335,8 +335,8 @@ async def async_step_calibrate(self, _user_input: dict[str, Any] | None = None): coordinator = await self.async_create_coordinator() # Let's see if we have already been configured before + device_name = self.data[CONF_NAME] entities = entity_registry.async_get(self.hass) - device_name = self.config_entry.data.get(CONF_NAME) climate_id = entities.async_get_entity_id(CLIMATE_DOMAIN, DOMAIN, device_name.lower()) async def start_calibration(): From 8c5564d82d4ecec103ba23df1b1ce4f07bfb8da7 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 01:34:22 +0100 Subject: [PATCH 144/213] Improve some logging --- custom_components/sat/mqtt/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 015b0461..e9616a94 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -115,12 +115,14 @@ def _process_message_payload(self, key: str, payload): async def _publish_command(self, payload: str): """Publish a command to the MQTT topic.""" - _LOGGER.debug(f"Publishing '{payload}' to MQTT.") + topic = self._get_topic_for_publishing() - if not self._simulation: + _LOGGER.debug("Publishing MQTT command: payload='%s', topic='%s'", payload, topic) + + if self._simulation: return try: - await mqtt.async_publish(self.hass, self._get_topic_for_publishing(), payload) - except Exception as e: - _LOGGER.error("Failed to publish command: %s", str(e)) + await mqtt.async_publish(self.hass, topic, payload) + except Exception as error: + _LOGGER.error("Failed to publish MQTT command. Error: %s", error) From 8ccbb9202c094d3774b3854e139617cfcf131f53 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 01:48:23 +0100 Subject: [PATCH 145/213] Improved MQTT discovery --- custom_components/sat/config_flow.py | 36 ++++++++++++---------- custom_components/sat/translations/de.json | 5 +-- custom_components/sat/translations/en.json | 5 +-- custom_components/sat/translations/es.json | 5 +-- custom_components/sat/translations/fr.json | 5 +-- custom_components/sat/translations/it.json | 5 +-- custom_components/sat/translations/nl.json | 5 +-- custom_components/sat/translations/pt.json | 5 +-- custom_components/sat/translations/sk.json | 5 +-- 9 files changed, 43 insertions(+), 33 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 3ad4d65c..a58d5c49 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -85,27 +85,28 @@ async def async_step_dhcp(self, discovery_info: DhcpServiceInfo): return await self.async_step_serial() async def async_step_mqtt(self, discovery_info: MqttServiceInfo): - """Handle dhcp discovery.""" - device_id = "unknown" - device_name = "unknown" + """Handle mqtt discovery.""" + _LOGGER.debug("Discovered at [mqtt://%s]", discovery_info.topic) - if discovery_info.topic[:5] == "OTGW/": - device_id = discovery_info.topic[11:] - device_name = "OTGW" + # Mapping topic prefixes to handler methods and device IDs + topic_mapping = { + "ems-esp/": ("ems-esp", self.async_step_mosquitto_ems), + "OTGW/": (discovery_info.topic[11:], self.async_step_mosquitto_opentherm), + } - if discovery_info.topic[:8] == "ems-esp/": - device_id = "ems-esp" - device_name = "EMS-ESP" + # Check for matching prefix and handle appropriately + for prefix, (device_id, step_method) in topic_mapping.items(): + if discovery_info.topic.startswith(prefix): + _LOGGER.debug("Identified gateway: %s", device_id) + self.data[CONF_DEVICE] = device_id - _LOGGER.debug("Discovered %s at [mqtt://%s]", device_name, discovery_info.topic) - self.data[CONF_DEVICE] = device_id + # Abort if the gateway is already registered, reload if necessary + await self.async_set_unique_id(device_id) - # abort if we already have exactly this gateway id/host - # reload the integration if the host got updated - await self.async_set_unique_id(device_id) - self._abort_if_unique_id_configured(updates=self.data) + return await step_method() - return await self.async_step_mosquitto() + _LOGGER.error("Unsupported MQTT topic format: %s", discovery_info.topic) + return self.async_abort(reason="unsupported_gateway") async def async_step_mosquitto(self, _user_input: dict[str, Any] | None = None): """Entry step to select the MQTT mode and branch to specific setup.""" @@ -134,7 +135,6 @@ async def async_step_mosquitto(self, _user_input: dict[str, Any] | None = None): ] ) ), - vol.Required(CONF_NAME, default=DEFAULT_NAME): str, }), ) @@ -149,6 +149,7 @@ async def async_step_mosquitto_opentherm(self, _user_input: dict[str, Any] | Non step_id="mosquitto_opentherm", last_step=False, data_schema=vol.Schema({ + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_MQTT_TOPIC, default="OTGW"): str, vol.Required(CONF_DEVICE, default="otgw-XXXXXXXXXXXX"): str, }), @@ -166,6 +167,7 @@ async def async_step_mosquitto_ems(self, _user_input: dict[str, Any] | None = No step_id="mosquitto_ems", last_step=False, data_schema=vol.Schema({ + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_MQTT_TOPIC, default="ems-esp"): str, }), ) diff --git a/custom_components/sat/translations/de.json b/custom_components/sat/translations/de.json index 312a8fdd..38bbadc5 100644 --- a/custom_components/sat/translations/de.json +++ b/custom_components/sat/translations/de.json @@ -55,14 +55,14 @@ "mosquitto": { "description": "Konfigurieren Sie den MQTT-Modus für Ihr Heizsystem. Wählen Sie den Gateway-Modus aus und geben Sie seinen Namen an.", "data": { - "mode": "Modus", - "name": "Name" + "mode": "Modus" }, "title": "MQTT-Konfiguration" }, "mosquitto_opentherm": { "description": "Richten Sie das OpenTherm Gateway ein. Geben Sie das Haupt-MQTT-Thema und die Gerätekennung an.", "data": { + "name": "Name", "mqtt_topic": "MQTT-Thema", "device": "Geräte-ID" }, @@ -71,6 +71,7 @@ "mosquitto_ems": { "description": "Richten Sie das EMS-ESP Gateway ein. Geben Sie das MQTT-Thema an.", "data": { + "name": "Name", "mqtt_topic": "MQTT-Thema" }, "title": "EMS-ESP Gateway Einrichtung" diff --git a/custom_components/sat/translations/en.json b/custom_components/sat/translations/en.json index f776a24c..4c053cc8 100644 --- a/custom_components/sat/translations/en.json +++ b/custom_components/sat/translations/en.json @@ -55,14 +55,14 @@ "mosquitto": { "description": "Configure the MQTT mode for your heating system. Select the gateway mode and provide its name.", "data": { - "mode": "Mode", - "name": "Name" + "mode": "Mode" }, "title": "MQTT Configuration" }, "mosquitto_opentherm": { "description": "Set up the OpenTherm Gateway. Provide the top MQTT topic and the device identifier.", "data": { + "name": "Name", "mqtt_topic": "MQTT Topic", "device": "Device ID" }, @@ -71,6 +71,7 @@ "mosquitto_ems": { "description": "Set up the EMS-ESP Gateway. Provide the MQTT topic.", "data": { + "name": "Name", "mqtt_topic": "MQTT Topic" }, "title": "EMS-ESP Gateway Setup" diff --git a/custom_components/sat/translations/es.json b/custom_components/sat/translations/es.json index 658be724..11aaf1a8 100644 --- a/custom_components/sat/translations/es.json +++ b/custom_components/sat/translations/es.json @@ -55,14 +55,14 @@ "mosquitto": { "description": "Configure el modo MQTT para su sistema de calefacción. Seleccione el modo de la puerta de enlace e introduzca su nombre.", "data": { - "mode": "Modo", - "name": "Nombre" + "mode": "Modo" }, "title": "Configuración MQTT" }, "mosquitto_opentherm": { "description": "Configure la Puerta de Enlace OpenTherm. Proporcione el tema principal de MQTT y el identificador del dispositivo.", "data": { + "name": "Nombre", "mqtt_topic": "Tema MQTT", "device": "ID del dispositivo" }, @@ -71,6 +71,7 @@ "mosquitto_ems": { "description": "Configure la Puerta de Enlace EMS-ESP. Proporcione el tema MQTT.", "data": { + "name": "Nombre", "mqtt_topic": "Tema MQTT" }, "title": "Configuración de la Puerta de Enlace EMS-ESP" diff --git a/custom_components/sat/translations/fr.json b/custom_components/sat/translations/fr.json index 02cc2e82..73873641 100644 --- a/custom_components/sat/translations/fr.json +++ b/custom_components/sat/translations/fr.json @@ -55,14 +55,14 @@ "mosquitto": { "description": "Configurez le mode MQTT pour votre système de chauffage. Sélectionnez le mode de la passerelle et fournissez son nom.", "data": { - "mode": "Mode", - "name": "Nom" + "mode": "Mode" }, "title": "Configuration MQTT" }, "mosquitto_opentherm": { "description": "Configurez la passerelle OpenTherm. Fournissez le sujet principal MQTT et l'identifiant de l'appareil.", "data": { + "name": "Nom", "mqtt_topic": "Sujet MQTT", "device": "ID de l'appareil" }, @@ -71,6 +71,7 @@ "mosquitto_ems": { "description": "Configurez la passerelle EMS-ESP. Fournissez le sujet MQTT.", "data": { + "name": "Nom", "mqtt_topic": "Sujet MQTT" }, "title": "Configuration de la Passerelle EMS-ESP" diff --git a/custom_components/sat/translations/it.json b/custom_components/sat/translations/it.json index 5e88da20..da00e646 100644 --- a/custom_components/sat/translations/it.json +++ b/custom_components/sat/translations/it.json @@ -55,14 +55,14 @@ "mosquitto": { "description": "Configura la modalità MQTT per il tuo sistema di riscaldamento. Seleziona la modalità del gateway e fornisci il suo nome.", "data": { - "mode": "Modalità", - "name": "Nome" + "mode": "Modalità" }, "title": "Configurazione MQTT" }, "mosquitto_opentherm": { "description": "Configura il Gateway OpenTherm. Fornisci l'argomento principale MQTT e l'identificativo del dispositivo.", "data": { + "name": "Nome", "mqtt_topic": "Argomento MQTT", "device": "ID del dispositivo" }, @@ -71,6 +71,7 @@ "mosquitto_ems": { "description": "Configura il Gateway EMS-ESP. Fornisci l'argomento MQTT.", "data": { + "name": "Nome", "mqtt_topic": "Argomento MQTT" }, "title": "Configurazione del Gateway EMS-ESP" diff --git a/custom_components/sat/translations/nl.json b/custom_components/sat/translations/nl.json index e7f43156..b4c67fcf 100644 --- a/custom_components/sat/translations/nl.json +++ b/custom_components/sat/translations/nl.json @@ -55,14 +55,14 @@ "mosquitto": { "description": "Configureer de MQTT-modus voor uw verwarmingssysteem. Kies de gateway-modus en geef de naam op.", "data": { - "mode": "Modus", - "name": "Naam" + "mode": "Modus" }, "title": "MQTT-configuratie" }, "mosquitto_opentherm": { "description": "Stel de OpenTherm Gateway in. Geef het belangrijkste MQTT-onderwerp en het apparaat-ID op.", "data": { + "name": "Naam", "mqtt_topic": "MQTT-onderwerp", "device": "Apparaat-ID" }, @@ -71,6 +71,7 @@ "mosquitto_ems": { "description": "Stel de EMS-ESP Gateway in. Geef het MQTT-onderwerp op.", "data": { + "name": "Naam", "mqtt_topic": "MQTT-onderwerp" }, "title": "EMS-ESP Gateway Configuratie" diff --git a/custom_components/sat/translations/pt.json b/custom_components/sat/translations/pt.json index 6bc85761..8d185bbd 100644 --- a/custom_components/sat/translations/pt.json +++ b/custom_components/sat/translations/pt.json @@ -55,14 +55,14 @@ "mosquitto": { "description": "Configure o modo MQTT para o seu sistema de aquecimento. Escolha o modo de gateway e forneça seu nome.", "data": { - "mode": "Modo", - "name": "Nome" + "mode": "Modo" }, "title": "Configuração MQTT" }, "mosquitto_opentherm": { "description": "Configure o Gateway OpenTherm. Forneça o tópico MQTT principal e o identificador do dispositivo.", "data": { + "name": "Nome", "mqtt_topic": "Tópico MQTT", "device": "ID do dispositivo" }, @@ -71,6 +71,7 @@ "mosquitto_ems": { "description": "Configure o Gateway EMS-ESP. Forneça o tópico MQTT.", "data": { + "name": "Nome", "mqtt_topic": "Tópico MQTT" }, "title": "Configuração do Gateway EMS-ESP" diff --git a/custom_components/sat/translations/sk.json b/custom_components/sat/translations/sk.json index ac890329..a247d276 100644 --- a/custom_components/sat/translations/sk.json +++ b/custom_components/sat/translations/sk.json @@ -55,14 +55,14 @@ "mosquitto": { "description": "Nakonfigurujte režim MQTT pre váš vykurovací systém. Vyberte režim brány a zadajte jej názov.", "data": { - "mode": "Režim", - "name": "Názov" + "mode": "Režim" }, "title": "Konfigurácia MQTT" }, "mosquitto_opentherm": { "description": "Nakonfigurujte OpenTherm Gateway. Uveďte hlavný predmet MQTT a identifikátor zariadenia.", "data": { + "name": "Názov", "mqtt_topic": "Predmet MQTT", "device": "ID zariadenia" }, @@ -71,6 +71,7 @@ "mosquitto_ems": { "description": "Nakonfigurujte EMS-ESP Gateway. Uveďte predmet MQTT.", "data": { + "name": "Názov", "mqtt_topic": "Predmet MQTT" }, "title": "Konfigurácia EMS-ESP Gateway" From 8611a69f621cee3ff2f4358dd23f5400e55e0c53 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 14:00:20 +0100 Subject: [PATCH 146/213] Make sure we also store the MODE when using discovery --- custom_components/sat/config_flow.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index a58d5c49..b12a5d95 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -90,14 +90,15 @@ async def async_step_mqtt(self, discovery_info: MqttServiceInfo): # Mapping topic prefixes to handler methods and device IDs topic_mapping = { - "ems-esp/": ("ems-esp", self.async_step_mosquitto_ems), - "OTGW/": (discovery_info.topic[11:], self.async_step_mosquitto_opentherm), + "ems-esp/": (MODE_MQTT_EMS, "ems-esp", self.async_step_mosquitto_ems), + "OTGW/": (MODE_MQTT_OPENTHERM, discovery_info.topic[11:], self.async_step_mosquitto_opentherm), } # Check for matching prefix and handle appropriately - for prefix, (device_id, step_method) in topic_mapping.items(): + for prefix, (mode, device_id, step_method) in topic_mapping.items(): if discovery_info.topic.startswith(prefix): - _LOGGER.debug("Identified gateway: %s", device_id) + _LOGGER.debug("Identified gateway type %s: %s", mode, device_id) + self.data[CONF_MODE] = mode self.data[CONF_DEVICE] = device_id # Abort if the gateway is already registered, reload if necessary @@ -281,7 +282,7 @@ async def async_step_sensors(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: self.data.update(_user_input) - if self.data[CONF_MODE] in [MODE_ESPHOME, MODE_MQTT_OPENTHERM, MODE_SERIAL, MODE_SIMULATOR]: + if self.data[CONF_MODE] in [MODE_ESPHOME, MODE_MQTT_OPENTHERM, MODE_MQTT_EMS, MODE_SERIAL, MODE_SIMULATOR]: return await self.async_step_heating_system() return await self.async_step_areas() From 41da622a7042f28079a510439aed53f80df0f39d Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 14:05:40 +0100 Subject: [PATCH 147/213] Add "forceheatingoff" to the ems coordinator --- custom_components/sat/mqtt/ems.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index c422a5d4..2d380885 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -7,6 +7,9 @@ from ..coordinator import DeviceState from ..util import float_value +DATA_ON = "on" +DATA_OFF = "off" + DATA_BOILER_DATA = "boiler_data" DATA_FLAME_ACTIVE = "burngas" DATA_DHW_SETPOINT = "dhw/seltemp" @@ -102,10 +105,12 @@ def get_tracked_entities(self) -> list[str]: async def async_set_control_setpoint(self, value: float) -> None: await self._publish_command(f'{{"cmd": "selflowtemp", "value": {0 if value == 10 else value}}}') + await super().async_set_control_setpoint(value) async def async_set_control_hot_water_setpoint(self, value: float) -> None: await self._publish_command(f'{{"cmd": "dhw/seltemp", "value": {value}}}') + await super().async_set_control_hot_water_setpoint(value) async def async_set_control_thermostat_setpoint(self, value: float) -> None: @@ -113,8 +118,7 @@ async def async_set_control_thermostat_setpoint(self, value: float) -> None: await super().async_set_control_thermostat_setpoint(value) async def async_set_heater_state(self, state: DeviceState) -> None: - if state == DeviceState.OFF: - await self.async_set_control_setpoint(0) + await self._publish_command(f'{{"cmd: "forceheatingoff", "value": {DATA_OFF if state == DeviceState.ON else DATA_ON}') await super().async_set_heater_state(state) From 24db858583e3d369624c41c90df48b7b1b0c6f94 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 14:06:00 +0100 Subject: [PATCH 148/213] Typo? --- custom_components/sat/mqtt/ems.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index 2d380885..fbabb74e 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -118,7 +118,7 @@ async def async_set_control_thermostat_setpoint(self, value: float) -> None: await super().async_set_control_thermostat_setpoint(value) async def async_set_heater_state(self, state: DeviceState) -> None: - await self._publish_command(f'{{"cmd: "forceheatingoff", "value": {DATA_OFF if state == DeviceState.ON else DATA_ON}') + await self._publish_command(f'{{"cmd: "forceheatingoff", "value": {DATA_OFF if state == DeviceState.ON else DATA_ON}}}') await super().async_set_heater_state(state) From 496bc2452ec7d1db881e9bd6eb0ddc417a5f29b0 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 14:24:52 +0100 Subject: [PATCH 149/213] Fixed simulation mode --- custom_components/sat/coordinator.py | 2 +- custom_components/sat/mqtt/__init__.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index a1b529f0..8ed75b57 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -74,7 +74,7 @@ def __init__(self, hass: HomeAssistant, data: Mapping[str, Any], options: Mappin self._options = options self._manufacturer = None self._device_state = DeviceState.OFF - self._simulation = bool(data.get(CONF_SIMULATION)) + self._simulation = bool(options.get(CONF_SIMULATION)) self._heating_system = str(data.get(CONF_HEATING_SYSTEM, HEATING_SYSTEM_UNKNOWN)) super().__init__(hass, _LOGGER, name=DOMAIN) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index e9616a94..5cb61ec4 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -22,9 +22,6 @@ class SatMqttCoordinator(ABC, SatDataUpdateCoordinator): def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], options: Mapping[str, Any] | None = None) -> None: super().__init__(hass, data, options) - _LOGGER.debug(snake_case(f"{self.__class__.__name__}")) - _LOGGER.debug(device_id) - self.data = {} self._device_id = device_id self._topic = data.get(CONF_MQTT_TOPIC) @@ -117,7 +114,7 @@ async def _publish_command(self, payload: str): """Publish a command to the MQTT topic.""" topic = self._get_topic_for_publishing() - _LOGGER.debug("Publishing MQTT command: payload='%s', topic='%s'", payload, topic) + _LOGGER.debug("Publishing MQTT command: payload='%s', topic='%s', simulation='%s'", payload, topic, self._simulation) if self._simulation: return From 37514bd1b31a427d1e709677fc155b4f85238feb Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 14:27:45 +0100 Subject: [PATCH 150/213] Bump version --- custom_components/sat/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index 7fec5fb5..ba3c90ca 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -1,7 +1,7 @@ # Base component constants NAME = "Smart Autotune Thermostat" DOMAIN = "sat" -VERSION = "3.0.x" +VERSION = "4.0.0-alpha" CLIMATE = "climate" COORDINATOR = "coordinator" CONFIG_STORE = "config_store" From 25af4bb9878f5ba34db2ae6629992dcc9a4b01d6 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 14:32:05 +0100 Subject: [PATCH 151/213] Make sure options is always a valid object --- custom_components/sat/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index 8ed75b57..7ad52c88 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -71,10 +71,10 @@ def __init__(self, hass: HomeAssistant, data: Mapping[str, Any], options: Mappin self.boiler_temperatures = [] self._data = data - self._options = options self._manufacturer = None + self._options = options or {} self._device_state = DeviceState.OFF - self._simulation = bool(options.get(CONF_SIMULATION)) + self._simulation = bool(self._options.get(CONF_SIMULATION)) self._heating_system = str(data.get(CONF_HEATING_SYSTEM, HEATING_SYSTEM_UNKNOWN)) super().__init__(hass, _LOGGER, name=DOMAIN) From 10174b338810133748ba577bcbd2dcacb903a744 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 14:39:26 +0100 Subject: [PATCH 152/213] Add missing fields --- custom_components/sat/mqtt/opentherm.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/sat/mqtt/opentherm.py b/custom_components/sat/mqtt/opentherm.py index ed8db4f6..215ea53d 100644 --- a/custom_components/sat/mqtt/opentherm.py +++ b/custom_components/sat/mqtt/opentherm.py @@ -145,6 +145,7 @@ async def boot(self) -> None: def get_tracked_entities(self) -> list[str]: return [ + DATA_SLAVE_MEMBERID, DATA_CENTRAL_HEATING, DATA_FLAME_ACTIVE, DATA_DHW_ENABLE, @@ -152,8 +153,10 @@ def get_tracked_entities(self) -> list[str]: DATA_CONTROL_SETPOINT, DATA_REL_MOD_LEVEL, DATA_BOILER_TEMPERATURE, + DATA_RETURN_TEMPERATURE, DATA_BOILER_CAPACITY, DATA_REL_MIN_MOD_LEVEL, + DATA_REL_MIN_MOD_LEVEL_LEGACY, DATA_MAX_REL_MOD_LEVEL_SETTING, DATA_DHW_SETPOINT_MINIMUM, DATA_DHW_SETPOINT_MAXIMUM, From 29ad4ac19b2f358dedb45b5b6d53326a7eeafee7 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 18:02:16 +0100 Subject: [PATCH 153/213] Add support for error handling --- custom_components/sat/__init__.py | 49 +++++++++++++++++++++- custom_components/sat/config_flow.py | 10 ++--- custom_components/sat/const.py | 4 +- custom_components/sat/manifest.json | 3 +- custom_components/sat/translations/de.json | 1 + custom_components/sat/translations/en.json | 1 + custom_components/sat/translations/es.json | 1 + custom_components/sat/translations/fr.json | 1 + custom_components/sat/translations/it.json | 1 + custom_components/sat/translations/nl.json | 1 + custom_components/sat/translations/pt.json | 1 + custom_components/sat/translations/sk.json | 1 + 12 files changed, 65 insertions(+), 9 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index 8c5e91d6..6ea5b838 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -1,5 +1,6 @@ import asyncio import logging +import traceback from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN @@ -9,6 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry from homeassistant.helpers.storage import Store +from sentry_sdk import Client, Hub from .const import * from .coordinator import SatDataUpdateCoordinatorFactory @@ -29,6 +31,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Create a new dictionary for this entry hass.data[DOMAIN][entry.entry_id] = {} + # Setup error monitoring (if enabled) + if entry.options.get(CONF_ERROR_MONITORING, True): + await hass.async_add_executor_job(initialize_sentry, hass) + # Resolve the coordinator by using the factory according to the mode hass.data[DOMAIN][entry.entry_id][COORDINATOR] = await SatDataUpdateCoordinatorFactory().resolve( hass=hass, data=entry.data, options=entry.options, mode=entry.data.get(CONF_MODE), device=entry.data.get(CONF_DEVICE) @@ -50,14 +56,20 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: This function is called by Home Assistant when the integration is being removed. """ - climate = hass.data[DOMAIN][entry.entry_id][CLIMATE] - await hass.data[DOMAIN][entry.entry_id][COORDINATOR].async_will_remove_from_hass(climate) + _climate = hass.data[DOMAIN][entry.entry_id][CLIMATE] + _coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] + + await _coordinator.async_will_remove_from_hass(_climate) unloaded = all( # Forward entry unload for used platforms await asyncio.gather(hass.config_entries.async_unload_platforms(entry, PLATFORMS)) ) + if SENTRY in hass.data[DOMAIN]: + hass.data[DOMAIN][SENTRY].flush() + hass.data[DOMAIN][SENTRY].close() + # Remove the entry from the data dictionary if all components are unloaded successfully if unloaded: hass.data[DOMAIN].pop(entry.entry_id) @@ -165,3 +177,36 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.info("Migration to version %s successful", entry.version) return True + + +def initialize_sentry(hass: HomeAssistant): + """Initialize Sentry synchronously in an offloaded executor job.""" + + def exception_filter(event, hint): + """Filter events to send only SAT-related exceptions to Sentry.""" + exc_info = hint.get("exc_info") + + if exc_info: + _, _, exc_traceback = exc_info + stack = traceback.extract_tb(exc_traceback) + + # Check if the exception originates from the SAT custom component + if any("custom_components/sat/" in frame.filename for frame in stack): + return event + + # Ignore exceptions not related to SAT + return None + + # Configure the Sentry client + client = Client( + traces_sample_rate=1.0, + before_send=exception_filter, + dsn="https://90e0ff6b2ca1f2fa4edcd34c5dd65808@o4508432869621760.ingest.de.sentry.io/4508432872898640", + ) + + # Bind the Sentry client to the Sentry hub + hub = Hub(client) + hub.bind_client(client) + + # Store the hub in Home Assistant's data for later use + hass.data[DOMAIN][SENTRY] = client diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index b12a5d95..8e70ca03 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -174,8 +174,6 @@ async def async_step_mosquitto_ems(self, _user_input: dict[str, Any] | None = No ) async def async_step_esphome(self, _user_input: dict[str, Any] | None = None): - self.errors = {} - if _user_input is not None: self.data.update(_user_input) self.data[CONF_MODE] = MODE_ESPHOME @@ -185,7 +183,6 @@ async def async_step_esphome(self, _user_input: dict[str, Any] | None = None): return self.async_show_form( step_id="esphome", last_step=False, - errors=self.errors, data_schema=vol.Schema({ vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_DEVICE, default=self.data.get(CONF_DEVICE)): selector.DeviceSelector( @@ -346,6 +343,7 @@ async def async_step_areas(self, _user_input: dict[str, Any] | None = None): )) return self.async_show_form( + last_step=False, step_id="areas", data_schema=vol.Schema({ vol.Optional(CONF_MAIN_CLIMATES, default=self.data.get(CONF_MAIN_CLIMATES, [])): climate_selector, @@ -493,12 +491,13 @@ async def async_step_finish(self, _user_input: dict[str, Any] | None = None): ) async def async_create_coordinator(self) -> SatDataUpdateCoordinator: - # Resolve the coordinator by using the factory according to the mode + """Resolve the coordinator by using the factory according to the mode""" return await SatDataUpdateCoordinatorFactory().resolve( hass=self.hass, data=self.data, mode=self.data[CONF_MODE], device=self.data[CONF_DEVICE] ) async def _enable_overshoot_protection(self, overshoot_protection_value: float): + """Store the value and enable overshoot protection.""" self.data[CONF_OVERSHOOT_PROTECTION] = True self.data[CONF_MINIMUM_SETPOINT] = overshoot_protection_value @@ -668,7 +667,8 @@ async def async_step_advanced(self, _user_input: dict[str, Any] | None = None): schema = { vol.Required(CONF_SIMULATION, default=options[CONF_SIMULATION]): bool, vol.Required(CONF_THERMAL_COMFORT, default=options[CONF_THERMAL_COMFORT]): bool, - vol.Required(CONF_DYNAMIC_MINIMUM_SETPOINT, default=options[CONF_DYNAMIC_MINIMUM_SETPOINT]): bool + vol.Required(CONF_ERROR_MONITORING, default=options[CONF_ERROR_MONITORING]): bool, + vol.Required(CONF_DYNAMIC_MINIMUM_SETPOINT, default=options[CONF_DYNAMIC_MINIMUM_SETPOINT]): bool, } if options.get(CONF_MODE) in [MODE_MQTT_OPENTHERM, MODE_SERIAL, MODE_SIMULATOR]: diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index ba3c90ca..bab445b0 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -3,6 +3,7 @@ DOMAIN = "sat" VERSION = "4.0.0-alpha" CLIMATE = "climate" +SENTRY = "sentry" COORDINATOR = "coordinator" CONFIG_STORE = "config_store" @@ -28,6 +29,7 @@ CONF_MODE = "mode" CONF_NAME = "name" CONF_DEVICE = "device" +CONF_ERROR_MONITORING = "error_monitoring" CONF_CYCLES_PER_HOUR = "cycles_per_hour" CONF_SIMULATED_HEATING = "simulated_heating" CONF_SIMULATED_COOLING = "simulated_cooling" @@ -90,10 +92,10 @@ HEATING_MODE_COMFORT = "comfort" OPTIONS_DEFAULTS = { - CONF_MODE: MODE_SERIAL, CONF_PROPORTIONAL: "45", CONF_INTEGRAL: "0", CONF_DERIVATIVE: "6000", + CONF_ERROR_MONITORING: True, CONF_CYCLES_PER_HOUR: 4, CONF_AUTOMATIC_GAINS: True, diff --git a/custom_components/sat/manifest.json b/custom_components/sat/manifest.json index 0e9e895b..5098edab 100644 --- a/custom_components/sat/manifest.json +++ b/custom_components/sat/manifest.json @@ -22,7 +22,8 @@ "OTGW/value/+" ], "requirements": [ - "pyotgw==2.1.3" + "pyotgw==2.1.3", + "sentry-sdk==2.19.2" ], "version": "4.0.0-alpha" } \ No newline at end of file diff --git a/custom_components/sat/translations/de.json b/custom_components/sat/translations/de.json index 38bbadc5..261158b5 100644 --- a/custom_components/sat/translations/de.json +++ b/custom_components/sat/translations/de.json @@ -147,6 +147,7 @@ "step": { "advanced": { "data": { + "error_monitoring": "Fehlerüberwachung aktivieren", "climate_valve_offset": "Klimaventil-Offset", "dynamic_minimum_setpoint": "Dynamischer Minimaler Sollwert (Experimentell)", "force_pulse_width_modulation": "Pulsweitenmodulation erzwingen", diff --git a/custom_components/sat/translations/en.json b/custom_components/sat/translations/en.json index 4c053cc8..a6e6b2dc 100644 --- a/custom_components/sat/translations/en.json +++ b/custom_components/sat/translations/en.json @@ -147,6 +147,7 @@ "step": { "advanced": { "data": { + "error_monitoring": "Enable error monitoring", "climate_valve_offset": "Climate valve offset", "dynamic_minimum_setpoint": "Dynamic Minimum Setpoint (Experimental)", "force_pulse_width_modulation": "Force Pulse Width Modulation", diff --git a/custom_components/sat/translations/es.json b/custom_components/sat/translations/es.json index 11aaf1a8..3359efea 100644 --- a/custom_components/sat/translations/es.json +++ b/custom_components/sat/translations/es.json @@ -147,6 +147,7 @@ "step": { "advanced": { "data": { + "error_monitoring": "Habilitar monitoreo de errores", "climate_valve_offset": "Compensación de la Válvula Climática", "dynamic_minimum_setpoint": "Punto de Ajuste Mínimo Dinámico (Experimental)", "force_pulse_width_modulation": "Forzar Modulación de Ancho de Pulso", diff --git a/custom_components/sat/translations/fr.json b/custom_components/sat/translations/fr.json index 73873641..135ab9b0 100644 --- a/custom_components/sat/translations/fr.json +++ b/custom_components/sat/translations/fr.json @@ -147,6 +147,7 @@ "step": { "advanced": { "data": { + "error_monitoring": "Activer la surveillance des erreurs", "climate_valve_offset": "Décalage de la vanne climatique", "dynamic_minimum_setpoint": "Point de Consigne Minimum Dynamique (Expérimental)", "force_pulse_width_modulation": "Forcer la Modulation de Largeur d'Impulsion", diff --git a/custom_components/sat/translations/it.json b/custom_components/sat/translations/it.json index da00e646..0d35eb57 100644 --- a/custom_components/sat/translations/it.json +++ b/custom_components/sat/translations/it.json @@ -147,6 +147,7 @@ "step": { "advanced": { "data": { + "error_monitoring": "Abilita monitoraggio errori", "climate_valve_offset": "Offset della Valvola Climatica", "dynamic_minimum_setpoint": "Setpoint Minimo Dinamico (Sperimentale)", "force_pulse_width_modulation": "Forzare la Modulazione di Larghezza di Impulso", diff --git a/custom_components/sat/translations/nl.json b/custom_components/sat/translations/nl.json index b4c67fcf..e877e522 100644 --- a/custom_components/sat/translations/nl.json +++ b/custom_components/sat/translations/nl.json @@ -147,6 +147,7 @@ "step": { "advanced": { "data": { + "error_monitoring": "Foutbewaking inschakelen", "climate_valve_offset": "Offset van Klimaatklep", "dynamic_minimum_setpoint": "Dynamisch Minimaal Setpoint (Experimenteel)", "force_pulse_width_modulation": "Dwing Pulsbreedtemodulatie af", diff --git a/custom_components/sat/translations/pt.json b/custom_components/sat/translations/pt.json index 8d185bbd..9034259f 100644 --- a/custom_components/sat/translations/pt.json +++ b/custom_components/sat/translations/pt.json @@ -147,6 +147,7 @@ "step": { "advanced": { "data": { + "error_monitoring": "Ativar monitoramento de erros", "climate_valve_offset": "Offset da válvula do clima", "dynamic_minimum_setpoint": "Setpoint Mínimo Dinâmico (Experimental)", "force_pulse_width_modulation": "Forçar Modulação por Largura de Pulso", diff --git a/custom_components/sat/translations/sk.json b/custom_components/sat/translations/sk.json index a247d276..a501da31 100644 --- a/custom_components/sat/translations/sk.json +++ b/custom_components/sat/translations/sk.json @@ -147,6 +147,7 @@ "step": { "advanced": { "data": { + "error_monitoring": "Povoliť monitorovanie chýb", "climate_valve_offset": "Offset klimatizačného ventilu", "dynamic_minimum_setpoint": "Dynamická minimálna nastavená hodnota (experimentálna)", "force_pulse_width_modulation": "Vynútená modulácia šírky impulzu", From dd1afa5909f9815ce2768b6ca321c53cba0da878 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 18:10:09 +0100 Subject: [PATCH 154/213] Add sentry_sdk to the list of requirements when testing --- requirements_test.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index fc866e30..38342bbe 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,8 @@ homeassistant aiohttp_cors aiodiscover aiodhcpwatcher +sentry_sdk freezegun pyotgw scapy -janus \ No newline at end of file +janus From 0858bca043cf2c060314f2794c77bdd2dfd2e45a Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 20:33:05 +0100 Subject: [PATCH 155/213] Fixed having data when calibrating --- custom_components/sat/climate.py | 2 +- custom_components/sat/config_flow.py | 1 + custom_components/sat/coordinator.py | 2 +- custom_components/sat/esphome/__init__.py | 6 +++--- custom_components/sat/mqtt/__init__.py | 5 ++--- custom_components/sat/serial/__init__.py | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 0e3bfdc5..0735a577 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -222,7 +222,7 @@ async def async_added_to_hass(self) -> None: await self._areas.async_added_to_hass(self.hass) # Let the coordinator know we are ready - await self._coordinator.async_added_to_hass(self) + await self._coordinator.async_added_to_hass() async def _register_event_listeners(self): """Register event listeners.""" diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 8e70ca03..d1583166 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -374,6 +374,7 @@ async def async_step_calibrate_system(self, _user_input: dict[str, Any] | None = async def async_step_calibrate(self, _user_input: dict[str, Any] | None = None): coordinator = await self.async_create_coordinator() + await coordinator.async_added_to_hass() # Let's see if we have already been configured before device_name = self.data[CONF_NAME] diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index 7ad52c88..49edf253 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -248,7 +248,7 @@ def supports_maximum_setpoint_management(self): """ return False - async def async_added_to_hass(self, climate: SatClimate) -> None: + async def async_added_to_hass(self) -> None: """Perform setup when the integration is added to Home Assistant.""" await self.async_set_control_max_setpoint(self.maximum_setpoint) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index fda68bd5..813a7777 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -39,7 +39,7 @@ DATA_MAX_REL_MOD_LEVEL_SETTING = "max_rel_mod_level" if TYPE_CHECKING: - from ..climate import SatClimate + pass _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -167,7 +167,7 @@ def member_id(self) -> int | None: return None - async def async_added_to_hass(self, climate: SatClimate) -> None: + async def async_added_to_hass(self) -> None: await mqtt.async_wait_for_mqtt_client(self.hass) # Create a list of entities that we track @@ -192,7 +192,7 @@ async def async_added_to_hass(self, climate: SatClimate) -> None: # Track those entities so the coordinator can be updated when something changes async_track_state_change_event(self.hass, entities, self.async_state_change_event) - await super().async_added_to_hass(climate) + await super().async_added_to_hass() async def async_state_change_event(self, _event: Event): if self._listeners: diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 5cb61ec4..2364c923 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -31,7 +31,7 @@ def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], def device_id(self) -> str: return self._device_id - async def async_added_to_hass(self, climate: SatClimate) -> None: + async def async_added_to_hass(self) -> None: await mqtt.async_wait_for_mqtt_client(self.hass) for key in self.get_tracked_entities(): @@ -43,7 +43,7 @@ async def async_added_to_hass(self, climate: SatClimate) -> None: await self.boot() - await super().async_added_to_hass(climate) + await super().async_added_to_hass() async def async_will_remove_from_hass(self, climate: SatClimate) -> None: # Save the updated data to persistent storage @@ -93,7 +93,6 @@ def _create_message_handler(self, key: str): @callback def message_handler(message): """Handle an incoming MQTT message and schedule an update.""" - _LOGGER.debug("Received MQTT message for key '%s': payload='%s'", key, message.payload) try: # Process the payload and update the data property diff --git a/custom_components/sat/serial/__init__.py b/custom_components/sat/serial/__init__.py index 075b8ff9..9d006f7f 100644 --- a/custom_components/sat/serial/__init__.py +++ b/custom_components/sat/serial/__init__.py @@ -166,10 +166,10 @@ async def async_connect(self) -> SatSerialCoordinator: return self - async def async_added_to_hass(self, climate: SatClimate) -> None: + async def async_added_to_hass(self) -> None: await self.async_connect() - await super().async_added_to_hass(climate) + await super().async_added_to_hass() async def async_will_remove_from_hass(self, climate: SatClimate) -> None: self._api.unsubscribe(self.async_set_updated_data) From 9fbe9372883d812be68461a85b85f7eddd2359c9 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 20:37:33 +0100 Subject: [PATCH 156/213] Fixed having data when calibrating --- custom_components/sat/__init__.py | 3 +-- custom_components/sat/climate.py | 2 +- custom_components/sat/config_flow.py | 7 +++++-- custom_components/sat/coordinator.py | 4 ++-- custom_components/sat/esphome/__init__.py | 6 +++--- custom_components/sat/mqtt/__init__.py | 6 +++--- custom_components/sat/serial/__init__.py | 4 ++-- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index aaf7cea9..811696ad 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -50,8 +50,7 @@ async def async_unload_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool: This function is called by Home Assistant when the integration is being removed. """ - climate = _hass.data[DOMAIN][_entry.entry_id][CLIMATE] - await _hass.data[DOMAIN][_entry.entry_id][COORDINATOR].async_will_remove_from_hass(climate) + await _hass.data[DOMAIN][_entry.entry_id][COORDINATOR].async_will_remove_from_hass() unloaded = all( # Forward entry unload for used platforms diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 41f428cf..3204ff71 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -217,7 +217,7 @@ async def async_added_to_hass(self) -> None: await self._minimum_setpoint.async_initialize(self.hass) # Let the coordinator know we are ready - await self._coordinator.async_added_to_hass(self) + await self._coordinator.async_added_to_hass() async def _register_event_listeners(self): """Register event listeners.""" diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index afee28d2..d30f4c51 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -332,8 +332,6 @@ async def async_step_calibrate_system(self, _user_input: dict[str, Any] | None = ) async def async_step_calibrate(self, _user_input: dict[str, Any] | None = None): - coordinator = await self.async_create_coordinator() - # Let's see if we have already been configured before device_name = self.data[CONF_NAME] entities = entity_registry.async_get(self.hass) @@ -341,8 +339,13 @@ async def async_step_calibrate(self, _user_input: dict[str, Any] | None = None): async def start_calibration(): try: + coordinator = await self.async_create_coordinator() + await coordinator.async_added_to_hass() + overshoot_protection = OvershootProtection(coordinator, self.data.get(CONF_HEATING_SYSTEM)) self.overshoot_protection_value = await overshoot_protection.calculate() + + await coordinator.async_will_remove_from_hass() except asyncio.TimeoutError: _LOGGER.warning("Calibration time-out.") return False diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index aea5c80d..0cb1fd09 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -244,11 +244,11 @@ def supports_maximum_setpoint_management(self): """ return False - async def async_added_to_hass(self, climate: SatClimate) -> None: + async def async_added_to_hass(self) -> None: """Perform setup when the integration is added to Home Assistant.""" await self.async_set_control_max_setpoint(self.maximum_setpoint) - async def async_will_remove_from_hass(self, climate: SatClimate) -> None: + async def async_will_remove_from_hass(self) -> None: """Run when an entity is removed from hass.""" pass diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index 544b8054..1142b3ce 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -39,7 +39,7 @@ DATA_MAX_REL_MOD_LEVEL_SETTING = "max_rel_mod_level" if TYPE_CHECKING: - from ..climate import SatClimate + pass _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -166,7 +166,7 @@ def member_id(self) -> int | None: return None - async def async_added_to_hass(self, climate: SatClimate) -> None: + async def async_added_to_hass(self) -> None: await mqtt.async_wait_for_mqtt_client(self.hass) # Create a list of entities that we track @@ -191,7 +191,7 @@ async def async_added_to_hass(self, climate: SatClimate) -> None: # Track those entities so the coordinator can be updated when something changes async_track_state_change_event(self.hass, entities, self.async_state_change_event) - await super().async_added_to_hass(climate) + await super().async_added_to_hass() async def async_state_change_event(self, _event: Event): if self._listeners: diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 6717e6a9..1dfbf5e8 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -32,7 +32,7 @@ DATA_DHW_SETPOINT_MAXIMUM = "TdhwSetUBTdhwSetLB_value_hb" if TYPE_CHECKING: - from ..climate import SatClimate + pass _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -170,7 +170,7 @@ async def boot(self) -> SatMqttCoordinator: return self - async def async_added_to_hass(self, climate: SatClimate) -> None: + async def async_added_to_hass(self) -> None: await mqtt.async_wait_for_mqtt_client(self.hass) # Create a list of entities that we track @@ -194,7 +194,7 @@ async def async_added_to_hass(self, climate: SatClimate) -> None: # Track those entities so the coordinator can be updated when something changes async_track_state_change_event(self.hass, entities, self.async_state_change_event) - await super().async_added_to_hass(climate) + await super().async_added_to_hass() async def async_state_change_event(self, _event: Event): if self._listeners: diff --git a/custom_components/sat/serial/__init__.py b/custom_components/sat/serial/__init__.py index 38f45741..4490aee5 100644 --- a/custom_components/sat/serial/__init__.py +++ b/custom_components/sat/serial/__init__.py @@ -13,7 +13,7 @@ from ..coordinator import DeviceState, SatDataUpdateCoordinator if TYPE_CHECKING: - from ..climate import SatClimate + pass _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -166,7 +166,7 @@ async def async_connect(self) -> SatSerialCoordinator: return self - async def async_will_remove_from_hass(self, climate: SatClimate) -> None: + async def async_will_remove_from_hass(self) -> None: self._api.unsubscribe(self.async_set_updated_data) await self._api.set_control_setpoint(0) From 4ae5abd71396fa59a373178601c8d8de879b59ac Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 20:38:46 +0100 Subject: [PATCH 157/213] Re-organized setting up for the overshoot protection --- custom_components/sat/__init__.py | 2 +- custom_components/sat/config_flow.py | 8 +++++--- custom_components/sat/coordinator.py | 2 +- custom_components/sat/mqtt/__init__.py | 3 +-- custom_components/sat/serial/__init__.py | 4 ++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index 6ea5b838..bd850268 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -59,7 +59,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _climate = hass.data[DOMAIN][entry.entry_id][CLIMATE] _coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] - await _coordinator.async_will_remove_from_hass(_climate) + await _coordinator.async_will_remove_from_hass() unloaded = all( # Forward entry unload for used platforms diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index d1583166..da63900e 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -373,9 +373,6 @@ async def async_step_calibrate_system(self, _user_input: dict[str, Any] | None = ) async def async_step_calibrate(self, _user_input: dict[str, Any] | None = None): - coordinator = await self.async_create_coordinator() - await coordinator.async_added_to_hass() - # Let's see if we have already been configured before device_name = self.data[CONF_NAME] entities = entity_registry.async_get(self.hass) @@ -383,8 +380,13 @@ async def async_step_calibrate(self, _user_input: dict[str, Any] | None = None): async def start_calibration(): try: + coordinator = await self.async_create_coordinator() + await coordinator.async_added_to_hass() + overshoot_protection = OvershootProtection(coordinator, self.data.get(CONF_HEATING_SYSTEM)) self.overshoot_protection_value = await overshoot_protection.calculate() + + await coordinator.async_will_remove_from_hass() except asyncio.TimeoutError: _LOGGER.warning("Calibration time-out.") return False diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index 49edf253..676be243 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -252,7 +252,7 @@ async def async_added_to_hass(self) -> None: """Perform setup when the integration is added to Home Assistant.""" await self.async_set_control_max_setpoint(self.maximum_setpoint) - async def async_will_remove_from_hass(self, climate: SatClimate) -> None: + async def async_will_remove_from_hass(self) -> None: """Run when an entity is removed from hass.""" pass diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 2364c923..fb9657b0 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -6,7 +6,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store -from ..climate import SatClimate from ..const import CONF_MQTT_TOPIC from ..coordinator import SatDataUpdateCoordinator from ..util import snake_case @@ -45,7 +44,7 @@ async def async_added_to_hass(self) -> None: await super().async_added_to_hass() - async def async_will_remove_from_hass(self, climate: SatClimate) -> None: + async def async_will_remove_from_hass(self) -> None: # Save the updated data to persistent storage await self._save_data() diff --git a/custom_components/sat/serial/__init__.py b/custom_components/sat/serial/__init__.py index 9d006f7f..277b0d1f 100644 --- a/custom_components/sat/serial/__init__.py +++ b/custom_components/sat/serial/__init__.py @@ -13,7 +13,7 @@ from ..coordinator import DeviceState, SatDataUpdateCoordinator if TYPE_CHECKING: - from ..climate import SatClimate + pass _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -171,7 +171,7 @@ async def async_added_to_hass(self) -> None: await super().async_added_to_hass() - async def async_will_remove_from_hass(self, climate: SatClimate) -> None: + async def async_will_remove_from_hass(self) -> None: self._api.unsubscribe(self.async_set_updated_data) await self._api.set_control_setpoint(0) From 4f50da9892b4f8d0511433ce174122e247b5ed91 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 20:50:56 +0100 Subject: [PATCH 158/213] Remove some obsolete code --- custom_components/sat/overshoot_protection.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 248e2b28..5e56bb4f 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -24,10 +24,7 @@ async def calculate(self) -> float | None: # Enforce timeouts to ensure operations do not run indefinitely await asyncio.wait_for(self._wait_for_flame(), timeout=OVERSHOOT_PROTECTION_INITIAL_WAIT) - await asyncio.wait_for(self._wait_for_stable_temperature(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) - - _LOGGER.info("Waiting an additional %s seconds for stability", STABLE_TEMPERATURE_WAIT) - await asyncio.sleep(STABLE_TEMPERATURE_WAIT) + await asyncio.wait_for(self._wait_for_stable_temperature(), timeout=STABLE_TEMPERATURE_WAIT) relative_modulation_value = await asyncio.wait_for(self._wait_for_stable_relative_modulation(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) From 09060301a0f634c96613c5d1a23d02dd8665d8ac Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 20:57:11 +0100 Subject: [PATCH 159/213] Improved some logging --- custom_components/sat/config_flow.py | 6 ++---- custom_components/sat/overshoot_protection.py | 5 ----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index da63900e..82147f83 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -388,11 +388,9 @@ async def start_calibration(): await coordinator.async_will_remove_from_hass() except asyncio.TimeoutError: - _LOGGER.warning("Calibration time-out.") - return False + _LOGGER.warning("Timed out during overshoot protection calculation.") except asyncio.CancelledError: - _LOGGER.warning("Cancelled calibration.") - return False + _LOGGER.warning("Cancelled overshoot protection calculation.") self.hass.async_create_task( self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 6dc608f8..6cb474a8 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -29,12 +29,7 @@ async def calculate(self) -> float | None: relative_modulation_value = await asyncio.wait_for(self._wait_for_stable_relative_modulation(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) return self._calculate_overshoot_value(relative_modulation_value) - except asyncio.TimeoutError as exception: - _LOGGER.warning("Timed out during overshoot protection calculation") - - raise exception except asyncio.CancelledError as exception: - _LOGGER.info("Calculation cancelled, shutting down heating system") await self._coordinator.async_set_heater_state(DeviceState.OFF) await self._coordinator.async_set_control_setpoint(MINIMUM_SETPOINT) From a7ebab9de001afd66bffbdc2f53e69a43fec038e Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 20:58:31 +0100 Subject: [PATCH 160/213] Increase timeout values --- custom_components/sat/overshoot_protection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 6cb474a8..cf86e74e 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -7,8 +7,8 @@ _LOGGER = logging.getLogger(__name__) OVERSHOOT_PROTECTION_TIMEOUT = 7200 # Two hours in seconds -OVERSHOOT_PROTECTION_INITIAL_WAIT = 180 # Three minutes in seconds -STABLE_TEMPERATURE_WAIT = 300 # Five minutes in seconds +OVERSHOOT_PROTECTION_INITIAL_WAIT = 300 # Five minutes in seconds +STABLE_TEMPERATURE_WAIT = 900 # Five teen minutes in seconds SLEEP_INTERVAL = 5 # Sleep interval in seconds From b0a0dd6374209e00d82610ae9893717d0689c694 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 21:15:08 +0100 Subject: [PATCH 161/213] Cleanup --- custom_components/sat/config_flow.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index d30f4c51..950721f4 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -347,17 +347,9 @@ async def start_calibration(): await coordinator.async_will_remove_from_hass() except asyncio.TimeoutError: - _LOGGER.warning("Calibration time-out.") - return False + _LOGGER.warning("Timed out during overshoot protection calculation.") except asyncio.CancelledError: - _LOGGER.warning("Cancelled calibration.") - return False - - self.hass.async_create_task( - self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) - ) - - return True + _LOGGER.warning("Cancelled overshoot protection calculation.") if not self.calibration: self.calibration = self.hass.async_create_task( From 27c2f76f12c4b70d71506cc57b993391ac166a5f Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 21:15:48 +0100 Subject: [PATCH 162/213] Cleanup --- custom_components/sat/config_flow.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 82147f83..f2f9ec56 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -392,12 +392,6 @@ async def start_calibration(): except asyncio.CancelledError: _LOGGER.warning("Cancelled overshoot protection calculation.") - self.hass.async_create_task( - self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) - ) - - return True - if not self.calibration: self.calibration = self.hass.async_create_task( start_calibration() From ca690301a8a1b12642a2e9402d9b7847cc04af8a Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 21:25:31 +0100 Subject: [PATCH 163/213] Making sure we have increased in temperature --- custom_components/sat/overshoot_protection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 5e56bb4f..f9586cae 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -48,13 +48,14 @@ async def _wait_for_flame(self) -> None: _LOGGER.info("Heating system has started") async def _wait_for_stable_temperature(self) -> None: + starting_temperature = float(self._coordinator.boiler_temperature) previous_average_temperature = float(self._coordinator.boiler_temperature) while True: current_temperature = float(self._coordinator.boiler_temperature) average_temperature, error_value = self._calculate_exponential_moving_average(previous_average_temperature, current_temperature) - if previous_average_temperature is not None and error_value <= DEADBAND: + if current_temperature > starting_temperature and previous_average_temperature is not None and error_value <= DEADBAND: _LOGGER.info("Stable temperature reached: %s°C", current_temperature) return From 09b1fc65de01a096a83f3afec5395fc2b1535503 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 21:48:02 +0100 Subject: [PATCH 164/213] Improve overshoot protection --- custom_components/sat/overshoot_protection.py | 85 +++++++++++-------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index f9586cae..5f4cd3da 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -1,96 +1,113 @@ import asyncio import logging -from custom_components.sat.const import * -from custom_components.sat.coordinator import DeviceState, SatDataUpdateCoordinator +from .const import OVERSHOOT_PROTECTION_SETPOINT, MINIMUM_SETPOINT, DEADBAND, MAXIMUM_RELATIVE_MOD +from .coordinator import DeviceState, SatDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +# Constants for timeouts and intervals OVERSHOOT_PROTECTION_TIMEOUT = 7200 # Two hours in seconds -OVERSHOOT_PROTECTION_INITIAL_WAIT = 180 # Three minutes in seconds -STABLE_TEMPERATURE_WAIT = 300 # Five minutes in seconds +OVERSHOOT_PROTECTION_INITIAL_WAIT = 300 # Five minutes in seconds +STABLE_TEMPERATURE_WAIT = 900 # Fifteen minutes in seconds SLEEP_INTERVAL = 5 # Sleep interval in seconds class OvershootProtection: def __init__(self, coordinator: SatDataUpdateCoordinator, heating_system: str): + """Initialize OvershootProtection with a coordinator and heating system configuration.""" self._alpha = 0.2 self._coordinator = coordinator - self._setpoint = OVERSHOOT_PROTECTION_SETPOINT[heating_system] + self._setpoint = OVERSHOOT_PROTECTION_SETPOINT.get(heating_system) + + if self._setpoint is None: + raise ValueError(f"Invalid heating system: {heating_system}") async def calculate(self) -> float | None: + """Calculate the overshoot protection value.""" try: _LOGGER.info("Starting overshoot protection calculation") - # Enforce timeouts to ensure operations do not run indefinitely + # Sequentially ensure the system is ready await asyncio.wait_for(self._wait_for_flame(), timeout=OVERSHOOT_PROTECTION_INITIAL_WAIT) - await asyncio.wait_for(self._wait_for_stable_temperature(), timeout=STABLE_TEMPERATURE_WAIT) + # Wait for a stable temperature relative_modulation_value = await asyncio.wait_for(self._wait_for_stable_relative_modulation(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) + await asyncio.wait_for(self._wait_for_stable_temperature(relative_modulation_value), timeout=STABLE_TEMPERATURE_WAIT) return self._calculate_overshoot_value(relative_modulation_value) - except asyncio.TimeoutError as exception: - _LOGGER.warning("Timed out during overshoot protection calculation") - - raise exception except asyncio.CancelledError as exception: - _LOGGER.info("Calculation cancelled, shutting down heating system") await self._coordinator.async_set_heater_state(DeviceState.OFF) await self._coordinator.async_set_control_setpoint(MINIMUM_SETPOINT) raise exception async def _wait_for_flame(self) -> None: - while not bool(self._coordinator.flame_active): + """Wait until the heating system flame is active.""" + while not self._coordinator.flame_active: _LOGGER.warning("Waiting for heating system to start") await self._trigger_heating_cycle() _LOGGER.info("Heating system has started") - async def _wait_for_stable_temperature(self) -> None: - starting_temperature = float(self._coordinator.boiler_temperature) - previous_average_temperature = float(self._coordinator.boiler_temperature) - - while True: - current_temperature = float(self._coordinator.boiler_temperature) - average_temperature, error_value = self._calculate_exponential_moving_average(previous_average_temperature, current_temperature) - - if current_temperature > starting_temperature and previous_average_temperature is not None and error_value <= DEADBAND: - _LOGGER.info("Stable temperature reached: %s°C", current_temperature) - return - - previous_average_temperature = average_temperature - await self._trigger_heating_cycle() - _LOGGER.debug("Temperature: %s°C, Error: %s°C", current_temperature, error_value) - async def _wait_for_stable_relative_modulation(self) -> float: + """Wait until the relative modulation stabilizes.""" previous_average_value = float(self._coordinator.relative_modulation_value) while True: current_value = float(self._coordinator.relative_modulation_value) average_value, error_value = self._calculate_exponential_moving_average(previous_average_value, current_value) - if previous_average_value is not None and error_value <= DEADBAND: - _LOGGER.info("Stable relative modulation reached: %s%%", current_value) + if error_value <= DEADBAND: + _LOGGER.info("Stable relative modulation reached: %.2f%%", current_value) return current_value previous_average_value = average_value await self._trigger_heating_cycle() _LOGGER.debug("Relative Modulation: %s%%, Error: %s%%", current_value, error_value) + async def _wait_for_stable_temperature(self, relative_modulation_value: float) -> None: + """Wait until the boiler temperature stabilizes, influenced by relative modulation.""" + starting_temperature = float(self._coordinator.boiler_temperature) + previous_average_temperature = float(self._coordinator.boiler_temperature) + + while True: + current_temperature = float(self._coordinator.boiler_temperature) + average_temperature, error_value = self._calculate_exponential_moving_average(previous_average_temperature, current_temperature) + + if current_temperature > starting_temperature and error_value <= DEADBAND: + _LOGGER.info("Stable temperature reached: %.2f°C", current_temperature) + return + + # Adjust heating cycle based on stable relative modulation + setpoint = (self._setpoint if relative_modulation_value == 0 else current_temperature) + await self._trigger_heating_cycle(setpoint) + + previous_average_temperature = average_temperature + _LOGGER.debug("Temperature: %s°C, Error: %s°C", current_temperature, error_value) + def _calculate_overshoot_value(self, relative_modulation_value: float) -> float: + """Calculate and log the overshoot value.""" overshoot_value = (100 - relative_modulation_value) / 100 * self._setpoint - _LOGGER.info("Calculated overshoot value: %s", overshoot_value) + _LOGGER.info("Calculated overshoot value: %.2f", overshoot_value) return overshoot_value def _calculate_exponential_moving_average(self, previous_average: float, current_value: float) -> tuple[float, float]: + """Calculate the exponential moving average and error.""" average_value = self._alpha * current_value + (1 - self._alpha) * previous_average error_value = abs(current_value - previous_average) return average_value, error_value - async def _trigger_heating_cycle(self) -> None: + async def _trigger_heating_cycle(self, setpoint: float = None) -> None: + """Trigger a heating cycle with the coordinator.""" await self._coordinator.async_set_heater_state(DeviceState.ON) - await self._coordinator.async_set_control_setpoint(self._setpoint) await self._coordinator.async_set_control_max_relative_modulation(MAXIMUM_RELATIVE_MOD) + await self._coordinator.async_set_control_setpoint(setpoint if setpoint is not None else self._setpoint) + await asyncio.sleep(SLEEP_INTERVAL) await self._coordinator.async_control_heating_loop() + + async def _reset_heater_state(self) -> None: + """Reset the heater state to default settings.""" + await self._coordinator.async_set_heater_state(DeviceState.OFF) + await self._coordinator.async_set_control_setpoint(MINIMUM_SETPOINT) From fabcc40c381577ceedbb8d2b10dc53b9dae76aa8 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 22:04:14 +0100 Subject: [PATCH 165/213] Improve overshoot protection --- custom_components/sat/overshoot_protection.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 5f4cd3da..91aca451 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -9,14 +9,14 @@ # Constants for timeouts and intervals OVERSHOOT_PROTECTION_TIMEOUT = 7200 # Two hours in seconds OVERSHOOT_PROTECTION_INITIAL_WAIT = 300 # Five minutes in seconds -STABLE_TEMPERATURE_WAIT = 900 # Fifteen minutes in seconds -SLEEP_INTERVAL = 5 # Sleep interval in seconds +OVERSHOOT_PROTECTION_STABLE_WAIT = 900 # Fifteen minutes in seconds +SLEEP_INTERVAL = 30 # Sleep interval in seconds class OvershootProtection: def __init__(self, coordinator: SatDataUpdateCoordinator, heating_system: str): """Initialize OvershootProtection with a coordinator and heating system configuration.""" - self._alpha = 0.2 + self._alpha = 0.5 self._coordinator = coordinator self._setpoint = OVERSHOOT_PROTECTION_SETPOINT.get(heating_system) @@ -29,11 +29,11 @@ async def calculate(self) -> float | None: _LOGGER.info("Starting overshoot protection calculation") # Sequentially ensure the system is ready - await asyncio.wait_for(self._wait_for_flame(), timeout=OVERSHOOT_PROTECTION_INITIAL_WAIT) + await asyncio.wait_for(self._wait_for_flame(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) # Wait for a stable temperature - relative_modulation_value = await asyncio.wait_for(self._wait_for_stable_relative_modulation(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) - await asyncio.wait_for(self._wait_for_stable_temperature(relative_modulation_value), timeout=STABLE_TEMPERATURE_WAIT) + relative_modulation_value = await asyncio.wait_for(self._wait_for_stable_relative_modulation(), timeout=OVERSHOOT_PROTECTION_STABLE_WAIT) + await asyncio.wait_for(self._wait_for_stable_temperature(relative_modulation_value), timeout=OVERSHOOT_PROTECTION_STABLE_WAIT) return self._calculate_overshoot_value(relative_modulation_value) except asyncio.CancelledError as exception: @@ -52,7 +52,7 @@ async def _wait_for_flame(self) -> None: async def _wait_for_stable_relative_modulation(self) -> float: """Wait until the relative modulation stabilizes.""" - previous_average_value = float(self._coordinator.relative_modulation_value) + previous_average_value = -1 while True: current_value = float(self._coordinator.relative_modulation_value) @@ -80,11 +80,11 @@ async def _wait_for_stable_temperature(self, relative_modulation_value: float) - return # Adjust heating cycle based on stable relative modulation - setpoint = (self._setpoint if relative_modulation_value == 0 else current_temperature) + setpoint = (self._setpoint if relative_modulation_value > 0 else current_temperature) await self._trigger_heating_cycle(setpoint) previous_average_temperature = average_temperature - _LOGGER.debug("Temperature: %s°C, Error: %s°C", current_temperature, error_value) + _LOGGER.debug("Temperature: %s°C, Error: %s°C", current_temperature, average_temperature) def _calculate_overshoot_value(self, relative_modulation_value: float) -> float: """Calculate and log the overshoot value.""" From 47cfbe44e61b1006928ea5d468b9fa63debec4e1 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 8 Dec 2024 23:26:05 +0100 Subject: [PATCH 166/213] Improve overshoot protection --- custom_components/sat/overshoot_protection.py | 71 ++++++++++--------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/custom_components/sat/overshoot_protection.py b/custom_components/sat/overshoot_protection.py index 91aca451..4fd7a38b 100644 --- a/custom_components/sat/overshoot_protection.py +++ b/custom_components/sat/overshoot_protection.py @@ -1,5 +1,6 @@ import asyncio import logging +import time from .const import OVERSHOOT_PROTECTION_SETPOINT, MINIMUM_SETPOINT, DEADBAND, MAXIMUM_RELATIVE_MOD from .coordinator import DeviceState, SatDataUpdateCoordinator @@ -7,10 +8,10 @@ _LOGGER = logging.getLogger(__name__) # Constants for timeouts and intervals -OVERSHOOT_PROTECTION_TIMEOUT = 7200 # Two hours in seconds OVERSHOOT_PROTECTION_INITIAL_WAIT = 300 # Five minutes in seconds OVERSHOOT_PROTECTION_STABLE_WAIT = 900 # Fifteen minutes in seconds -SLEEP_INTERVAL = 30 # Sleep interval in seconds +OVERSHOOT_PROTECTION_RELATIVE_MODULATION_WAIT = 300 # Five minutes in seconds +SLEEP_INTERVAL = 15 # Sleep interval in seconds class OvershootProtection: @@ -18,6 +19,7 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, heating_system: str): """Initialize OvershootProtection with a coordinator and heating system configuration.""" self._alpha = 0.5 self._coordinator = coordinator + self._stable_temperature = None self._setpoint = OVERSHOOT_PROTECTION_SETPOINT.get(heating_system) if self._setpoint is None: @@ -29,13 +31,16 @@ async def calculate(self) -> float | None: _LOGGER.info("Starting overshoot protection calculation") # Sequentially ensure the system is ready - await asyncio.wait_for(self._wait_for_flame(), timeout=OVERSHOOT_PROTECTION_TIMEOUT) + await asyncio.wait_for(self._wait_for_flame(), timeout=OVERSHOOT_PROTECTION_INITIAL_WAIT) # Wait for a stable temperature - relative_modulation_value = await asyncio.wait_for(self._wait_for_stable_relative_modulation(), timeout=OVERSHOOT_PROTECTION_STABLE_WAIT) - await asyncio.wait_for(self._wait_for_stable_temperature(relative_modulation_value), timeout=OVERSHOOT_PROTECTION_STABLE_WAIT) + await asyncio.wait_for(self._wait_for_stable_temperature(), timeout=OVERSHOOT_PROTECTION_STABLE_WAIT) - return self._calculate_overshoot_value(relative_modulation_value) + # Wait a bit before calculating the overshoot value, if required + if self._coordinator.relative_modulation_value > 0: + await self._wait_a_moment(OVERSHOOT_PROTECTION_RELATIVE_MODULATION_WAIT) + + return self._calculate_overshoot_value() except asyncio.CancelledError as exception: await self._coordinator.async_set_heater_state(DeviceState.OFF) await self._coordinator.async_set_control_setpoint(MINIMUM_SETPOINT) @@ -46,51 +51,47 @@ async def _wait_for_flame(self) -> None: """Wait until the heating system flame is active.""" while not self._coordinator.flame_active: _LOGGER.warning("Waiting for heating system to start") - await self._trigger_heating_cycle() + await self._trigger_heating_cycle(is_ready=False) _LOGGER.info("Heating system has started") - async def _wait_for_stable_relative_modulation(self) -> float: + async def _wait_a_moment(self, wait_time: int) -> None: """Wait until the relative modulation stabilizes.""" - previous_average_value = -1 - - while True: - current_value = float(self._coordinator.relative_modulation_value) - average_value, error_value = self._calculate_exponential_moving_average(previous_average_value, current_value) - - if error_value <= DEADBAND: - _LOGGER.info("Stable relative modulation reached: %.2f%%", current_value) - return current_value - previous_average_value = average_value - await self._trigger_heating_cycle() - _LOGGER.debug("Relative Modulation: %s%%, Error: %s%%", current_value, error_value) + start_time = time.time() + while time.time() - start_time < wait_time: + await self._trigger_heating_cycle(True) + await asyncio.sleep(SLEEP_INTERVAL) - async def _wait_for_stable_temperature(self, relative_modulation_value: float) -> None: + async def _wait_for_stable_temperature(self) -> None: """Wait until the boiler temperature stabilizes, influenced by relative modulation.""" - starting_temperature = float(self._coordinator.boiler_temperature) - previous_average_temperature = float(self._coordinator.boiler_temperature) + while not self._coordinator.boiler_temperature: + _LOGGER.warning("Waiting for boiler temperature") + + starting_temperature = self._coordinator.boiler_temperature + previous_average_temperature = self._coordinator.boiler_temperature while True: current_temperature = float(self._coordinator.boiler_temperature) average_temperature, error_value = self._calculate_exponential_moving_average(previous_average_temperature, current_temperature) if current_temperature > starting_temperature and error_value <= DEADBAND: + self._stable_temperature = current_temperature _LOGGER.info("Stable temperature reached: %.2f°C", current_temperature) return - # Adjust heating cycle based on stable relative modulation - setpoint = (self._setpoint if relative_modulation_value > 0 else current_temperature) - await self._trigger_heating_cycle(setpoint) + await self._trigger_heating_cycle(is_ready=True) previous_average_temperature = average_temperature - _LOGGER.debug("Temperature: %s°C, Error: %s°C", current_temperature, average_temperature) + _LOGGER.warning("Waiting for a stable temperature") + _LOGGER.debug("Temperature: %s°C, Error: %s°C", current_temperature, error_value) - def _calculate_overshoot_value(self, relative_modulation_value: float) -> float: + def _calculate_overshoot_value(self) -> float: """Calculate and log the overshoot value.""" - overshoot_value = (100 - relative_modulation_value) / 100 * self._setpoint - _LOGGER.info("Calculated overshoot value: %.2f", overshoot_value) - return overshoot_value + if self._coordinator.relative_modulation_value == 0: + return self._stable_temperature + + return (100 - self._coordinator.relative_modulation_value) / 100 * self._setpoint def _calculate_exponential_moving_average(self, previous_average: float, current_value: float) -> tuple[float, float]: """Calculate the exponential moving average and error.""" @@ -98,15 +99,19 @@ def _calculate_exponential_moving_average(self, previous_average: float, current error_value = abs(current_value - previous_average) return average_value, error_value - async def _trigger_heating_cycle(self, setpoint: float = None) -> None: + async def _trigger_heating_cycle(self, is_ready: bool) -> None: """Trigger a heating cycle with the coordinator.""" await self._coordinator.async_set_heater_state(DeviceState.ON) + await self._coordinator.async_set_control_setpoint(await self._get_setpoint(is_ready)) await self._coordinator.async_set_control_max_relative_modulation(MAXIMUM_RELATIVE_MOD) - await self._coordinator.async_set_control_setpoint(setpoint if setpoint is not None else self._setpoint) await asyncio.sleep(SLEEP_INTERVAL) await self._coordinator.async_control_heating_loop() + async def _get_setpoint(self, is_ready) -> float: + """Get the setpoint for the heating cycle.""" + return self._setpoint if not is_ready or self._coordinator.relative_modulation_value > 0 else self._coordinator.boiler_temperature + async def _reset_heater_state(self) -> None: """Reset the heater state to default settings.""" await self._coordinator.async_set_heater_state(DeviceState.OFF) From fbb87b71f754d2a3655c517ec591c527470e57d7 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds <1023654+Alexwijn@users.noreply.github.com> Date: Tue, 10 Dec 2024 23:06:35 +0100 Subject: [PATCH 167/213] Update custom_components/sat/mqtt/ems.py Co-authored-by: ahhoj --- custom_components/sat/mqtt/ems.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index fbabb74e..57edb7d5 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -118,7 +118,7 @@ async def async_set_control_thermostat_setpoint(self, value: float) -> None: await super().async_set_control_thermostat_setpoint(value) async def async_set_heater_state(self, state: DeviceState) -> None: - await self._publish_command(f'{{"cmd: "forceheatingoff", "value": {DATA_OFF if state == DeviceState.ON else DATA_ON}}}') + await self._publish_command(f'{{"cmd": "heatingoff", "value": "{DATA_OFF if state == DeviceState.ON else DATA_ON}"}}') await super().async_set_heater_state(state) From 883277c6e6bcdfaaa5cb48bc01e72f1d29bec510 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 11 Dec 2024 20:56:29 +0100 Subject: [PATCH 168/213] Make sure we don't allow duplicated gateways --- custom_components/sat/config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index f2f9ec56..3d05adc2 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -86,7 +86,7 @@ async def async_step_dhcp(self, discovery_info: DhcpServiceInfo): async def async_step_mqtt(self, discovery_info: MqttServiceInfo): """Handle mqtt discovery.""" - _LOGGER.debug("Discovered at [mqtt://%s]", discovery_info.topic) + _LOGGER.debug("Discovered MQTT at [mqtt://%s]", discovery_info.topic) # Mapping topic prefixes to handler methods and device IDs topic_mapping = { @@ -97,12 +97,13 @@ async def async_step_mqtt(self, discovery_info: MqttServiceInfo): # Check for matching prefix and handle appropriately for prefix, (mode, device_id, step_method) in topic_mapping.items(): if discovery_info.topic.startswith(prefix): - _LOGGER.debug("Identified gateway type %s: %s", mode, device_id) + _LOGGER.debug("Identified gateway type %s: %s", mode[5:], device_id) self.data[CONF_MODE] = mode self.data[CONF_DEVICE] = device_id # Abort if the gateway is already registered, reload if necessary await self.async_set_unique_id(device_id) + self._abort_if_unique_id_configured(updates=self.data) return await step_method() From 900c381f2719815d915d27106e6d296675922cb3 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 11 Dec 2024 21:03:08 +0100 Subject: [PATCH 169/213] Only listen to the parent ems-esp --- custom_components/sat/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/manifest.json b/custom_components/sat/manifest.json index 5098edab..91804cd5 100644 --- a/custom_components/sat/manifest.json +++ b/custom_components/sat/manifest.json @@ -18,7 +18,7 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/Alexwijn/SAT/issues", "mqtt": [ - "ems-esp/+", + "ems-esp/#", "OTGW/value/+" ], "requirements": [ From 92714b522d8b9a71d7155d5f4135e5f86070ec8d Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 11 Dec 2024 21:08:43 +0100 Subject: [PATCH 170/213] Improve logging --- custom_components/sat/coordinator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index 676be243..24c6cc52 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -276,22 +276,22 @@ async def async_set_heater_state(self, state: DeviceState) -> None: async def async_set_control_setpoint(self, value: float) -> None: """Control the boiler setpoint temperature for the device.""" if self.supports_setpoint_management: - _LOGGER.info("Set control boiler setpoint to %d", value) + _LOGGER.info("Set control boiler setpoint to %d°C", value) async def async_set_control_hot_water_setpoint(self, value: float) -> None: """Control the DHW setpoint temperature for the device.""" if self.supports_hot_water_setpoint_management: - _LOGGER.info("Set control hot water setpoint to %d", value) + _LOGGER.info("Set control hot water setpoint to %d°C", value) async def async_set_control_max_setpoint(self, value: float) -> None: """Control the maximum setpoint temperature for the device.""" if self.supports_maximum_setpoint_management: - _LOGGER.info("Set maximum setpoint to %d", value) + _LOGGER.info("Set maximum setpoint to %d°C", value) async def async_set_control_max_relative_modulation(self, value: int) -> None: """Control the maximum relative modulation for the device.""" if self.supports_relative_modulation_management: - _LOGGER.info("Set maximum relative modulation to %d", value) + _LOGGER.info("Set maximum relative modulation to %d%", value) async def async_set_control_thermostat_setpoint(self, value: float) -> None: """Control the setpoint temperature for the thermostat.""" From 644f356ee75f8d7e6952e030082c92dc82245550 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 11 Dec 2024 21:21:37 +0100 Subject: [PATCH 171/213] Cleaning up --- custom_components/sat/__init__.py | 2 +- custom_components/sat/config_flow.py | 43 ++++++++++++++-------------- custom_components/sat/coordinator.py | 2 +- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index bd850268..9aff490c 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await hass.async_add_executor_job(initialize_sentry, hass) # Resolve the coordinator by using the factory according to the mode - hass.data[DOMAIN][entry.entry_id][COORDINATOR] = await SatDataUpdateCoordinatorFactory().resolve( + hass.data[DOMAIN][entry.entry_id][COORDINATOR] = SatDataUpdateCoordinatorFactory().resolve( hass=hass, data=entry.data, options=entry.options, mode=entry.data.get(CONF_MODE), device=entry.data.get(CONF_DEVICE) ) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 3d05adc2..a40be06a 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -147,15 +147,7 @@ async def async_step_mosquitto_opentherm(self, _user_input: dict[str, Any] | Non return await self.async_step_sensors() - return self.async_show_form( - step_id="mosquitto_opentherm", - last_step=False, - data_schema=vol.Schema({ - vol.Required(CONF_NAME, default=DEFAULT_NAME): str, - vol.Required(CONF_MQTT_TOPIC, default="OTGW"): str, - vol.Required(CONF_DEVICE, default="otgw-XXXXXXXXXXXX"): str, - }), - ) + return self._create_mqtt_form("mosquitto_opentherm", "OTGW", "otgw-XXXXXXXXXXXX") async def async_step_mosquitto_ems(self, _user_input: dict[str, Any] | None = None): """Setup specific to EMS-ESP.""" @@ -165,14 +157,7 @@ async def async_step_mosquitto_ems(self, _user_input: dict[str, Any] | None = No return await self.async_step_sensors() - return self.async_show_form( - step_id="mosquitto_ems", - last_step=False, - data_schema=vol.Schema({ - vol.Required(CONF_NAME, default=DEFAULT_NAME): str, - vol.Required(CONF_MQTT_TOPIC, default="ems-esp"): str, - }), - ) + return self._create_mqtt_form("mosquitto_ems", "ems-esp") async def async_step_esphome(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: @@ -418,7 +403,7 @@ async def start_calibration(): if self.overshoot_protection_value is None: return self.async_abort(reason="unable_to_calibrate") - await self._enable_overshoot_protection( + self._enable_overshoot_protection( self.overshoot_protection_value ) @@ -441,7 +426,7 @@ async def async_step_calibrated(self, _user_input: dict[str, Any] | None = None) async def async_step_overshoot_protection(self, _user_input: dict[str, Any] | None = None): if _user_input is not None: - await self._enable_overshoot_protection( + self._enable_overshoot_protection( _user_input[CONF_MINIMUM_SETPOINT] ) @@ -488,11 +473,27 @@ async def async_step_finish(self, _user_input: dict[str, Any] | None = None): async def async_create_coordinator(self) -> SatDataUpdateCoordinator: """Resolve the coordinator by using the factory according to the mode""" - return await SatDataUpdateCoordinatorFactory().resolve( + return SatDataUpdateCoordinatorFactory().resolve( hass=self.hass, data=self.data, mode=self.data[CONF_MODE], device=self.data[CONF_DEVICE] ) - async def _enable_overshoot_protection(self, overshoot_protection_value: float): + def _create_mqtt_form(self, step_id: str, default_topic: str, default_device: str = None): + """Create a common MQTT configuration form.""" + schema = { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_MQTT_TOPIC, default=default_topic): str, + } + + if default_device: + schema[vol.Required(CONF_DEVICE, default=default_device)] = str + + return self.async_show_form( + step_id=step_id, + last_step=False, + data_schema=vol.Schema(schema), + ) + + def _enable_overshoot_protection(self, overshoot_protection_value: float): """Store the value and enable overshoot protection.""" self.data[CONF_OVERSHOOT_PROTECTION] = True self.data[CONF_MINIMUM_SETPOINT] = overshoot_protection_value diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index 24c6cc52..56555b71 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -27,7 +27,7 @@ class DeviceState(str, Enum): class SatDataUpdateCoordinatorFactory: @staticmethod - async def resolve( + def resolve( hass: HomeAssistant, mode: str, device: str, From a1aa736aba57e0985124943d969ff4c63d089a28 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 11 Dec 2024 21:38:25 +0100 Subject: [PATCH 172/213] Add some more logging --- custom_components/sat/coordinator.py | 2 +- custom_components/sat/relative_modulation.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index 56555b71..8014e544 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -291,7 +291,7 @@ async def async_set_control_max_setpoint(self, value: float) -> None: async def async_set_control_max_relative_modulation(self, value: int) -> None: """Control the maximum relative modulation for the device.""" if self.supports_relative_modulation_management: - _LOGGER.info("Set maximum relative modulation to %d%", value) + _LOGGER.info("Set maximum relative modulation to %d%%", value) async def async_set_control_thermostat_setpoint(self, value: float) -> None: """Control the setpoint temperature for the thermostat.""" diff --git a/custom_components/sat/relative_modulation.py b/custom_components/sat/relative_modulation.py index 59b43533..ef9affea 100644 --- a/custom_components/sat/relative_modulation.py +++ b/custom_components/sat/relative_modulation.py @@ -1,9 +1,12 @@ +import logging from enum import Enum from . import MINIMUM_SETPOINT, HEATING_SYSTEM_HEAT_PUMP from .coordinator import SatDataUpdateCoordinator from .pwm import PWMState +_LOGGER = logging.getLogger(__name__) + # Enum to represent different states of relative modulation class RelativeModulationState(str, Enum): @@ -17,16 +20,20 @@ class RelativeModulationState(str, Enum): class RelativeModulation: def __init__(self, coordinator: SatDataUpdateCoordinator, heating_system: str): """Initialize instance variables""" - self._heating_system = heating_system # The heating system that is being controlled - self._pwm_state = None # Tracks the current state of the PWM (Pulse Width Modulation) system - self._warming_up = False # Stores data related to the warming-up state of the heating system - self._coordinator = coordinator # Reference to the data coordinator responsible for system-wide information + self._heating_system = heating_system + self._pwm_state = None + self._warming_up = False + self._coordinator = coordinator + + _LOGGER.debug("Relative Modulation initialized for heating system: %s", heating_system) async def update(self, warming_up: bool, state: PWMState) -> None: """Update internal state with new data received from the coordinator""" self._pwm_state = state self._warming_up = warming_up + _LOGGER.debug("Updated Relative Modulation: enabled=%s, state=%s", self.enabled, self.state) + @property def state(self) -> RelativeModulationState: """Determine the current state of relative modulation based on coordinator and internal data""" From accaf58f665c0204906722d3e97f065b7e8ec7ad Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 11 Dec 2024 21:54:38 +0100 Subject: [PATCH 173/213] Making sure that all commands are received --- custom_components/sat/mqtt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index fb9657b0..db3cf0cf 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -118,6 +118,6 @@ async def _publish_command(self, payload: str): return try: - await mqtt.async_publish(self.hass, topic, payload) + await mqtt.async_publish(hass=self.hass, topic=topic, payload=payload, qos=1) except Exception as error: _LOGGER.error("Failed to publish MQTT command. Error: %s", error) From e8825d3aa96a705f809a4178481301763475fac4 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 11 Dec 2024 22:00:30 +0100 Subject: [PATCH 174/213] Make sure we also load the MQTT data --- custom_components/sat/mqtt/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index db3cf0cf..85a7fcee 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -31,6 +31,8 @@ def device_id(self) -> str: return self._device_id async def async_added_to_hass(self) -> None: + await self._load_stored_data() + await mqtt.async_wait_for_mqtt_client(self.hass) for key in self.get_tracked_entities(): From b284c0d33337b82b6ed506acdf1ffbace1184c8d Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 11 Dec 2024 22:22:37 +0100 Subject: [PATCH 175/213] Make sure we do not load any invalid data --- custom_components/sat/mqtt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 85a7fcee..64790acb 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -62,7 +62,7 @@ async def async_notify_listeners(self): async def _load_stored_data(self) -> None: """Load the data from persistent storage.""" if stored_data := await self._store.async_load(): - self.data.update(stored_data) + self.data.update({key: value for key, value in stored_data.items() if value not in (None, "")}) async def _save_data(self) -> None: """Save the data to persistent storage.""" From 5a6c49e26c10938461981c26c01b6f46de1ca347 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 11 Dec 2024 22:22:49 +0100 Subject: [PATCH 176/213] Make sure we wait a bit before sending the next command --- custom_components/sat/mqtt/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 64790acb..3e8044c0 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -1,3 +1,4 @@ +import asyncio import logging from abc import ABC, abstractmethod from typing import Mapping, Any @@ -110,7 +111,7 @@ def _process_message_payload(self, key: str, payload): """Process and store the payload of a received MQTT message.""" self.data[key] = payload - async def _publish_command(self, payload: str): + async def _publish_command(self, payload: str, wait_time: float = 0.5): """Publish a command to the MQTT topic.""" topic = self._get_topic_for_publishing() @@ -121,5 +122,8 @@ async def _publish_command(self, payload: str): try: await mqtt.async_publish(hass=self.hass, topic=topic, payload=payload, qos=1) + + # Add a small delay to allow processing of the message + await asyncio.sleep(wait_time) except Exception as error: _LOGGER.error("Failed to publish MQTT command. Error: %s", error) From 650ec7b9c7ae8e68628be8e6c95dc64d8d1874bb Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 11 Dec 2024 22:37:02 +0100 Subject: [PATCH 177/213] Update logging for consistency --- custom_components/sat/minimum_setpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/minimum_setpoint.py b/custom_components/sat/minimum_setpoint.py index 63f0a4af..e195de18 100644 --- a/custom_components/sat/minimum_setpoint.py +++ b/custom_components/sat/minimum_setpoint.py @@ -46,7 +46,7 @@ def calculate(self, return_temperature: float) -> None: adjustment = (return_temperature - self.base_return_temperature) * self.adjustment_factor self.current_minimum_setpoint = self.configured_minimum_setpoint + adjustment - _LOGGER.debug(f"Calculated new minimum setpoint: {self.current_minimum_setpoint}") + _LOGGER.debug(f"Calculated new minimum setpoint: %d°C", self.current_minimum_setpoint) def current(self) -> float: return self.current_minimum_setpoint if self.current_minimum_setpoint is not None else self.configured_minimum_setpoint From 0ac4153bb4a0bb0f7b0ee3839b4e3d2525a7b6c8 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 11 Dec 2024 22:48:45 +0100 Subject: [PATCH 178/213] Cleanup --- custom_components/sat/__init__.py | 10 +++++++++- custom_components/sat/minimum_setpoint.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index 9aff490c..6de2df7a 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -12,7 +12,15 @@ from homeassistant.helpers.storage import Store from sentry_sdk import Client, Hub -from .const import * +from .const import ( + DOMAIN, + CLIMATE, + SENTRY, + COORDINATOR, + CONF_MODE, + CONF_DEVICE, + CONF_ERROR_MONITORING, +) from .coordinator import SatDataUpdateCoordinatorFactory _LOGGER: logging.Logger = logging.getLogger(__name__) diff --git a/custom_components/sat/minimum_setpoint.py b/custom_components/sat/minimum_setpoint.py index e195de18..d73dc41b 100644 --- a/custom_components/sat/minimum_setpoint.py +++ b/custom_components/sat/minimum_setpoint.py @@ -46,7 +46,7 @@ def calculate(self, return_temperature: float) -> None: adjustment = (return_temperature - self.base_return_temperature) * self.adjustment_factor self.current_minimum_setpoint = self.configured_minimum_setpoint + adjustment - _LOGGER.debug(f"Calculated new minimum setpoint: %d°C", self.current_minimum_setpoint) + _LOGGER.debug("Calculated new minimum setpoint: %d°C", self.current_minimum_setpoint) def current(self) -> float: return self.current_minimum_setpoint if self.current_minimum_setpoint is not None else self.configured_minimum_setpoint From 62a668dfc90a27e418f9412a0ef8475bdcdf92dd Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Wed, 11 Dec 2024 22:50:41 +0100 Subject: [PATCH 179/213] Typo? --- custom_components/sat/relative_modulation.py | 2 +- custom_components/sat/simulator/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/relative_modulation.py b/custom_components/sat/relative_modulation.py index ef9affea..3a979dec 100644 --- a/custom_components/sat/relative_modulation.py +++ b/custom_components/sat/relative_modulation.py @@ -1,7 +1,7 @@ import logging from enum import Enum -from . import MINIMUM_SETPOINT, HEATING_SYSTEM_HEAT_PUMP +from .const import MINIMUM_SETPOINT, HEATING_SYSTEM_HEAT_PUMP from .coordinator import SatDataUpdateCoordinator from .pwm import PWMState diff --git a/custom_components/sat/simulator/__init__.py b/custom_components/sat/simulator/__init__.py index 29521b63..a1c99df9 100644 --- a/custom_components/sat/simulator/__init__.py +++ b/custom_components/sat/simulator/__init__.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant -from .. import CONF_SIMULATED_HEATING, CONF_SIMULATED_COOLING, MINIMUM_SETPOINT, CONF_SIMULATED_WARMING_UP, CONF_MAXIMUM_SETPOINT +from ..const import CONF_SIMULATED_HEATING, CONF_SIMULATED_COOLING, MINIMUM_SETPOINT, CONF_SIMULATED_WARMING_UP, CONF_MAXIMUM_SETPOINT from ..coordinator import DeviceState, SatDataUpdateCoordinator from ..util import convert_time_str_to_seconds From d25d8cfc7b17b82fc35e75bdbc0ec923962c1ba9 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 14 Dec 2024 16:15:29 +0100 Subject: [PATCH 180/213] Fixed the tests --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2db936f1..19e31ff4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ from homeassistant.setup import async_setup_component from pytest_homeassistant_custom_component.common import assert_setup_component, MockConfigEntry -from custom_components.sat import DOMAIN, CLIMATE, COORDINATOR +from custom_components.sat.const import DOMAIN, CLIMATE, COORDINATOR from custom_components.sat.climate import SatClimate from custom_components.sat.fake import SatFakeCoordinator from tests.const import DEFAULT_USER_DATA From db84033c0821a16c4824b783660840ffa4024f38 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 14 Dec 2024 16:17:17 +0100 Subject: [PATCH 181/213] Update some EMS sensors --- custom_components/sat/mqtt/ems.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index 57edb7d5..b36db355 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -18,11 +18,11 @@ DATA_BOILER_TEMPERATURE = "curflowtemp" DATA_RETURN_TEMPERATURE = "rettemp" -DATA_DHW_ENABLE = "tapwateractive" +DATA_DHW_ENABLE = "tapactivated" DATA_CENTRAL_HEATING = "heatingactive" DATA_BOILER_CAPACITY = "nompower" -DATA_REL_MIN_MOD_LEVEL = "burnminnpower" +DATA_REL_MIN_MOD_LEVEL = "burnminpower" DATA_MAX_REL_MOD_LEVEL_SETTING = "burnmaxpower" _LOGGER: logging.Logger = logging.getLogger(__name__) From fdad866a12c0b760bcbc6e9a1c8342ecdf0f6f83 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 14 Dec 2024 16:40:58 +0100 Subject: [PATCH 182/213] More test fixes --- tests/const.py | 13 +++++++++++-- tests/test_climate.py | 6 +++--- tests/test_config_flow.py | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/const.py b/tests/const.py index 891884de..932dff53 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,5 +1,14 @@ -from custom_components.sat import CONF_NAME, CONF_DEVICE, CONF_INSIDE_SENSOR_ENTITY_ID, CONF_OUTSIDE_SENSOR_ENTITY_ID, CONF_MODE, MODE_FAKE, CONF_AUTOMATIC_GAINS, \ - CONF_AUTOMATIC_DUTY_CYCLE, CONF_OVERSHOOT_PROTECTION +from custom_components.sat.const import ( + MODE_FAKE, + CONF_DEVICE, + CONF_NAME, + CONF_INSIDE_SENSOR_ENTITY_ID, + CONF_OUTSIDE_SENSOR_ENTITY_ID, + CONF_MODE, + CONF_AUTOMATIC_GAINS, + CONF_AUTOMATIC_DUTY_CYCLE, + CONF_OVERSHOOT_PROTECTION +) DEFAULT_USER_DATA = { CONF_MODE: MODE_FAKE, diff --git a/tests/test_climate.py b/tests/test_climate.py index d407c330..b82b4636 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -42,7 +42,7 @@ }, )], ]) -async def test_scenario_1(hass: HomeAssistant, entry: MockConfigEntry, climate: SatClimate, coordinator: SatFakeCoordinator) -> None: +async def test_scenario_1(_hass: HomeAssistant, entry: MockConfigEntry, climate: SatClimate, coordinator: SatFakeCoordinator) -> None: await coordinator.async_set_boiler_temperature(57) await climate.async_set_target_temperature(21.0) await climate.async_set_hvac_mode(HVACMode.HEAT) @@ -85,7 +85,7 @@ async def test_scenario_1(hass: HomeAssistant, entry: MockConfigEntry, climate: }, )], ]) -async def test_scenario_2(hass: HomeAssistant, entry: MockConfigEntry, climate: SatClimate, coordinator: SatFakeCoordinator) -> None: +async def test_scenario_2(_hass: HomeAssistant, entry: MockConfigEntry, climate: SatClimate, coordinator: SatFakeCoordinator) -> None: await coordinator.async_set_boiler_temperature(58) await climate.async_set_target_temperature(19.0) await climate.async_set_hvac_mode(HVACMode.HEAT) @@ -129,7 +129,7 @@ async def test_scenario_2(hass: HomeAssistant, entry: MockConfigEntry, climate: }, )], ]) -async def test_scenario_3(hass: HomeAssistant, entry: MockConfigEntry, climate: SatClimate, coordinator: SatFakeCoordinator) -> None: +async def test_scenario_3(_hass: HomeAssistant, entry: MockConfigEntry, climate: SatClimate, coordinator: SatFakeCoordinator) -> None: await coordinator.async_set_boiler_temperature(41) await climate.async_set_target_temperature(20.0) await climate.async_set_hvac_mode(HVACMode.HEAT) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index a785ab07..23c28de4 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,8 +1,8 @@ -from custom_components.sat import MODE_FAKE +from custom_components.sat.const import MODE_FAKE from custom_components.sat.config_flow import SatFlowHandler -async def test_create_coordinator(hass): +async def test_create_coordinator(_hass): flow_handler = SatFlowHandler() flow_handler.data = { "name": "Test", From c8a638d70bc7ab5fb65160874645ecdbaeacf6d3 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 14 Dec 2024 16:47:54 +0100 Subject: [PATCH 183/213] Only send "heatingoff" when it is not in sync --- custom_components/sat/mqtt/ems.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index b36db355..92d1aade 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -118,7 +118,8 @@ async def async_set_control_thermostat_setpoint(self, value: float) -> None: await super().async_set_control_thermostat_setpoint(value) async def async_set_heater_state(self, state: DeviceState) -> None: - await self._publish_command(f'{{"cmd": "heatingoff", "value": "{DATA_OFF if state == DeviceState.ON else DATA_ON}"}}') + if (state == DeviceState.ON) != self.device_active: + await self._publish_command(f'{{"cmd": "heatingoff", "value": "{DATA_OFF if state == DeviceState.ON else DATA_ON}"}}') await super().async_set_heater_state(state) From 54f58b61c33edb9d374305d91014e595296ef8d3 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 14 Dec 2024 16:48:48 +0100 Subject: [PATCH 184/213] Fix some more sensors --- custom_components/sat/mqtt/ems.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index 92d1aade..67704c24 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -49,15 +49,15 @@ def supports_relative_modulation_management(self) -> bool: @property def device_active(self) -> bool: - return bool(self.data.get(DATA_CENTRAL_HEATING)) + return self.data.get(DATA_CENTRAL_HEATING) == DATA_ON @property def flame_active(self) -> bool: - return bool(self.data.get(DATA_FLAME_ACTIVE)) + return self.data.get(DATA_FLAME_ACTIVE) == DATA_ON @property def hot_water_active(self) -> bool: - return bool(self.data.get(DATA_DHW_ENABLE)) + return self.data.get(DATA_DHW_ENABLE) == DATA_ON @property def setpoint(self) -> float | None: From 5671b773d4acf714dde3f32e809691f21747b7c6 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 14 Dec 2024 16:55:33 +0100 Subject: [PATCH 185/213] Fix tests? --- tests/test_climate.py | 3 +++ tests/test_config_flow.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_climate.py b/tests/test_climate.py index b82b4636..34657f98 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -17,6 +17,7 @@ [( [(TEMPLATE_DOMAIN, 1)], { + CONF_MODE: MODE_FAKE, CONF_HEATING_SYSTEM: HEATING_SYSTEM_RADIATORS, CONF_MINIMUM_SETPOINT: 57, CONF_MAXIMUM_SETPOINT: 75, @@ -60,6 +61,7 @@ async def test_scenario_1(_hass: HomeAssistant, entry: MockConfigEntry, climate: [( [(TEMPLATE_DOMAIN, 1)], { + CONF_MODE: MODE_FAKE, CONF_HEATING_SYSTEM: HEATING_SYSTEM_RADIATORS, CONF_MINIMUM_SETPOINT: 58, CONF_MAXIMUM_SETPOINT: 75 @@ -104,6 +106,7 @@ async def test_scenario_2(_hass: HomeAssistant, entry: MockConfigEntry, climate: [( [(TEMPLATE_DOMAIN, 1)], { + CONF_MODE: MODE_FAKE, CONF_HEATING_SYSTEM: HEATING_SYSTEM_RADIATORS, CONF_MINIMUM_SETPOINT: 41, CONF_MAXIMUM_SETPOINT: 75, diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 23c28de4..57f2ca17 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -2,7 +2,7 @@ from custom_components.sat.config_flow import SatFlowHandler -async def test_create_coordinator(_hass): +async def test_create_coordinator(hass): flow_handler = SatFlowHandler() flow_handler.data = { "name": "Test", From 2d01d03de180b117367cdb3e34cc0e67ba9a90a7 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 14 Dec 2024 16:57:21 +0100 Subject: [PATCH 186/213] Fix tests? --- tests/test_climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_climate.py b/tests/test_climate.py index 34657f98..c95bd225 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -43,7 +43,7 @@ }, )], ]) -async def test_scenario_1(_hass: HomeAssistant, entry: MockConfigEntry, climate: SatClimate, coordinator: SatFakeCoordinator) -> None: +async def test_scenario_1(hass: HomeAssistant, entry: MockConfigEntry, climate: SatClimate, coordinator: SatFakeCoordinator) -> None: await coordinator.async_set_boiler_temperature(57) await climate.async_set_target_temperature(21.0) await climate.async_set_hvac_mode(HVACMode.HEAT) @@ -87,7 +87,7 @@ async def test_scenario_1(_hass: HomeAssistant, entry: MockConfigEntry, climate: }, )], ]) -async def test_scenario_2(_hass: HomeAssistant, entry: MockConfigEntry, climate: SatClimate, coordinator: SatFakeCoordinator) -> None: +async def test_scenario_2(hass: HomeAssistant, entry: MockConfigEntry, climate: SatClimate, coordinator: SatFakeCoordinator) -> None: await coordinator.async_set_boiler_temperature(58) await climate.async_set_target_temperature(19.0) await climate.async_set_hvac_mode(HVACMode.HEAT) @@ -132,7 +132,7 @@ async def test_scenario_2(_hass: HomeAssistant, entry: MockConfigEntry, climate: }, )], ]) -async def test_scenario_3(_hass: HomeAssistant, entry: MockConfigEntry, climate: SatClimate, coordinator: SatFakeCoordinator) -> None: +async def test_scenario_3(hass: HomeAssistant, entry: MockConfigEntry, climate: SatClimate, coordinator: SatFakeCoordinator) -> None: await coordinator.async_set_boiler_temperature(41) await climate.async_set_target_temperature(20.0) await climate.async_set_hvac_mode(HVACMode.HEAT) From a91769edbfbd8f08cd71abe7486383c850d3e90d Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 22 Dec 2024 14:00:15 +0100 Subject: [PATCH 187/213] Make sure the burnmaxpower is at least 20 --- custom_components/sat/mqtt/ems.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index 67704c24..73dd1784 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -124,7 +124,7 @@ async def async_set_heater_state(self, state: DeviceState) -> None: await super().async_set_heater_state(state) async def async_set_control_max_relative_modulation(self, value: int) -> None: - await self._publish_command(f'{{"cmd": "burnmaxpower", "value": {value}}}') + await self._publish_command(f'{{"cmd": "burnmaxpower", "value": {min(value, 20)}}}') await super().async_set_control_max_relative_modulation(value) From efc5af8a26c1c9cc654c534f4b0f6c45a86e2140 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 22 Dec 2024 14:00:43 +0100 Subject: [PATCH 188/213] Improve the heater management by checking the current value --- custom_components/sat/climate.py | 39 +++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 0735a577..2652c2f8 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -800,10 +800,20 @@ async def _async_control_setpoint(self, pwm_state: PWMState) -> None: _LOGGER.info("Control setpoint has been updated to: %.1f°C", self._setpoint) async def _async_control_relative_modulation(self) -> None: - """Control the relative modulation value based on the conditions""" - if self._coordinator.supports_relative_modulation_management: - await self._relative_modulation.update(self.warming_up, self.pwm.state) - await self._coordinator.async_set_control_max_relative_modulation(self.relative_modulation_value) + """Control the relative modulation value based on the conditions.""" + if not self._coordinator.supports_relative_modulation_management: + _LOGGER.debug("Relative modulation management is not supported. Skipping control.") + return + + # Update relative modulation state + await self._relative_modulation.update(self.warming_up, self.pwm.state) + + # Determine if the value needs to be updated + if self._coordinator.relative_modulation_value == self.relative_modulation_value: + _LOGGER.debug("Relative modulation value unchanged (%d%%). No update necessary.", self.relative_modulation_value) + return + + await self._coordinator.async_set_control_max_relative_modulation(self.relative_modulation_value) async def _async_update_rooms_from_climates(self) -> None: """Update the temperature setpoint for each room based on their associated climate entity.""" @@ -904,11 +914,24 @@ async def async_control_heating_loop(self, _time=None) -> None: self.async_write_ha_state() async def async_set_heater_state(self, state: DeviceState): - if state == DeviceState.ON and not self.valves_open: - _LOGGER.warning('No valves are open at the moment.') - return await self._coordinator.async_set_heater_state(DeviceState.OFF) + """Set the heater state, ensuring proper conditions are met.""" + _LOGGER.debug("Attempting to set heater state to: %s", state) + + if state == DeviceState.ON: + if self._coordinator.device_active: + _LOGGER.info("Heater is already active. No action taken.") + return + + if not self.valves_open: + _LOGGER.warning("Cannot turn on heater: no valves are open.") + return + + elif state == DeviceState.OFF: + if not self._coordinator.device_active: + _LOGGER.info("Heater is already off. No action taken.") + return - return await self._coordinator.async_set_heater_state(state) + await self._coordinator.async_set_heater_state(state) async def async_set_temperature(self, **kwargs) -> None: """Set the target temperature.""" From 610c6fea501ed9b39aafeb4af9e790df695a5131 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 22 Dec 2024 14:05:56 +0100 Subject: [PATCH 189/213] Typo? --- custom_components/sat/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 2652c2f8..22c254a5 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -809,7 +809,7 @@ async def _async_control_relative_modulation(self) -> None: await self._relative_modulation.update(self.warming_up, self.pwm.state) # Determine if the value needs to be updated - if self._coordinator.relative_modulation_value == self.relative_modulation_value: + if self._coordinator.maximum_relative_modulation_value == self.relative_modulation_value: _LOGGER.debug("Relative modulation value unchanged (%d%%). No update necessary.", self.relative_modulation_value) return From f4c791084cc4bcbd52231dbd9352fedd6b536ca2 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 22 Dec 2024 14:37:39 +0100 Subject: [PATCH 190/213] Revert some code since the parent is handling it now --- custom_components/sat/mqtt/ems.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index 73dd1784..24793c27 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -118,8 +118,7 @@ async def async_set_control_thermostat_setpoint(self, value: float) -> None: await super().async_set_control_thermostat_setpoint(value) async def async_set_heater_state(self, state: DeviceState) -> None: - if (state == DeviceState.ON) != self.device_active: - await self._publish_command(f'{{"cmd": "heatingoff", "value": "{DATA_OFF if state == DeviceState.ON else DATA_ON}"}}') + await self._publish_command(f'{{"cmd": "heatingoff", "value": "{DATA_OFF if state == DeviceState.ON else DATA_ON}"}}') await super().async_set_heater_state(state) From 25bddb9f1c2205f508598bf35aabd8606b6c9008 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 24 Dec 2024 16:39:20 +0100 Subject: [PATCH 191/213] Disable sentry by default --- custom_components/sat/__init__.py | 2 +- custom_components/sat/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index 6de2df7a..7cfdf6f5 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -209,7 +209,7 @@ def exception_filter(event, hint): client = Client( traces_sample_rate=1.0, before_send=exception_filter, - dsn="https://90e0ff6b2ca1f2fa4edcd34c5dd65808@o4508432869621760.ingest.de.sentry.io/4508432872898640", + dsn="https://216fc0a74c488abdb79f9839fb7da33e@o4508432869621760.ingest.de.sentry.io/4508432872898640", ) # Bind the Sentry client to the Sentry hub diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index bab445b0..e0a40009 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -95,7 +95,7 @@ CONF_PROPORTIONAL: "45", CONF_INTEGRAL: "0", CONF_DERIVATIVE: "6000", - CONF_ERROR_MONITORING: True, + CONF_ERROR_MONITORING: False, CONF_CYCLES_PER_HOUR: 4, CONF_AUTOMATIC_GAINS: True, From 27798e14a17b442b7af00ccd5336c7ef8fb3ba30 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 24 Dec 2024 16:53:51 +0100 Subject: [PATCH 192/213] Added some sanity checks --- custom_components/sat/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/sat/sensor.py b/custom_components/sat/sensor.py index da742420..095c3fec 100644 --- a/custom_components/sat/sensor.py +++ b/custom_components/sat/sensor.py @@ -26,6 +26,10 @@ async def async_setup_entry(_hass: HomeAssistant, _config_entry: ConfigEntry, _a """ Add sensors for the serial protocol if the integration is set to use it. """ + # Some sanity checks before we continue + if any(key not in _hass.data[DOMAIN][_config_entry.entry_id] for key in (CLIMATE, COORDINATOR)): + return + climate = _hass.data[DOMAIN][_config_entry.entry_id][CLIMATE] coordinator = _hass.data[DOMAIN][_config_entry.entry_id][COORDINATOR] From 1106c4e9321bc18558c392ba6ca6ea95a8c87645 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 24 Dec 2024 16:57:34 +0100 Subject: [PATCH 193/213] Update version since it's not alpha anymore --- custom_components/sat/const.py | 2 +- custom_components/sat/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index e0a40009..e4126aca 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -1,7 +1,7 @@ # Base component constants NAME = "Smart Autotune Thermostat" DOMAIN = "sat" -VERSION = "4.0.0-alpha" +VERSION = "4.0.0" CLIMATE = "climate" SENTRY = "sentry" COORDINATOR = "coordinator" diff --git a/custom_components/sat/manifest.json b/custom_components/sat/manifest.json index 91804cd5..61e532c0 100644 --- a/custom_components/sat/manifest.json +++ b/custom_components/sat/manifest.json @@ -25,5 +25,5 @@ "pyotgw==2.1.3", "sentry-sdk==2.19.2" ], - "version": "4.0.0-alpha" + "version": "4.0.0" } \ No newline at end of file From 2c0c6bc0bae2dfe126284334bd5d54905d2df8a3 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 24 Dec 2024 17:37:58 +0100 Subject: [PATCH 194/213] Add support for Device Info --- custom_components/sat/__init__.py | 3 +++ custom_components/sat/const.py | 1 - custom_components/sat/coordinator.py | 9 +++++++++ custom_components/sat/entity.py | 16 +++++++++------- custom_components/sat/esphome/__init__.py | 10 +++++++--- custom_components/sat/fake/__init__.py | 4 ++++ custom_components/sat/mqtt/__init__.py | 3 ++- custom_components/sat/mqtt/ems.py | 4 ++++ custom_components/sat/mqtt/opentherm.py | 4 ++++ custom_components/sat/serial/__init__.py | 4 ++++ custom_components/sat/simulator/__init__.py | 4 ++++ custom_components/sat/switch/__init__.py | 4 ++++ 12 files changed, 54 insertions(+), 12 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index 7cfdf6f5..999a806e 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -48,6 +48,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass=hass, data=entry.data, options=entry.options, mode=entry.data.get(CONF_MODE), device=entry.data.get(CONF_DEVICE) ) + # Making sure everything is loaded + await hass.data[DOMAIN][entry.entry_id][COORDINATOR].setup() + # Forward entry setup for used platforms await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index e4126aca..c786e33f 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -1,7 +1,6 @@ # Base component constants NAME = "Smart Autotune Thermostat" DOMAIN = "sat" -VERSION = "4.0.0" CLIMATE = "climate" SENTRY = "sentry" COORDINATOR = "coordinator" diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index 8014e544..94f1e7d7 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -84,6 +84,11 @@ def __init__(self, hass: HomeAssistant, data: Mapping[str, Any], options: Mappin def device_id(self) -> str: pass + @property + @abstractmethod + def device_type(self) -> str: + pass + @property def device_state(self): """Return the current state of the device.""" @@ -248,6 +253,10 @@ def supports_maximum_setpoint_management(self): """ return False + async def setup(self) -> None: + """Perform setup when the integration is about to be added to Home Assistant.""" + pass + async def async_added_to_hass(self) -> None: """Perform setup when the integration is added to Home Assistant.""" await self.async_set_control_max_setpoint(self.maximum_setpoint) diff --git a/custom_components/sat/entity.py b/custom_components/sat/entity.py index c166dcc4..9b0b4dd2 100644 --- a/custom_components/sat/entity.py +++ b/custom_components/sat/entity.py @@ -4,9 +4,10 @@ import typing from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, NAME, VERSION, CONF_NAME +from .const import DOMAIN, NAME, CONF_NAME _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -24,12 +25,13 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn @property def device_info(self): - return { - "name": NAME, - "model": VERSION, - "manufacturer": NAME, - "identifiers": {(DOMAIN, self._config_entry.data.get(CONF_NAME))}, - } + return DeviceInfo( + name=NAME, + suggested_area="Living Room", + model=self._coordinator.device_type, + manufacturer=self._coordinator.manufacturer.name, + identifiers={(DOMAIN, self._config_entry.data.get(CONF_NAME))} + ) class SatClimateEntity(SatEntity): diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index 813a7777..d6370151 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -62,6 +62,10 @@ def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], def device_id(self) -> str: return self._mac_address + @property + def device_type(self) -> str: + return "ESPHome" + @property def supports_setpoint_management(self): return True @@ -138,19 +142,19 @@ def relative_modulation_value(self) -> float | None: return float(value) return super().relative_modulation_value - + @property def boiler_capacity(self) -> float | None: if (value := self.get(SENSOR_DOMAIN, DATA_BOILER_CAPACITY)) is not None: return float(value) return super().boiler_capacity - + @property def minimum_relative_modulation_value(self) -> float | None: if (value := self.get(SENSOR_DOMAIN, DATA_REL_MIN_MOD_LEVEL)) is not None: return float(value) - + return super().minimum_relative_modulation_value @property diff --git a/custom_components/sat/fake/__init__.py b/custom_components/sat/fake/__init__.py index 83371a85..cbdf8372 100644 --- a/custom_components/sat/fake/__init__.py +++ b/custom_components/sat/fake/__init__.py @@ -29,6 +29,10 @@ class SatFakeCoordinator(SatDataUpdateCoordinator): def device_id(self) -> str: return "Fake" + @property + def device_type(self) -> str: + return "Fake" + @property def member_id(self) -> int | None: return -1 diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 3e8044c0..65c7ef33 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -31,9 +31,10 @@ def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], def device_id(self) -> str: return self._device_id - async def async_added_to_hass(self) -> None: + async def setup(self): await self._load_stored_data() + async def async_added_to_hass(self) -> None: await mqtt.async_wait_for_mqtt_client(self.hass) for key in self.get_tracked_entities(): diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index 24793c27..800ebafc 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -31,6 +31,10 @@ class SatEmsMqttCoordinator(SatMqttCoordinator): """Class to manage fetching data from the OTGW Gateway using MQTT.""" + @property + def device_type(self) -> str: + return "Energy Management System (via mqtt)" + @property def supports_setpoint_management(self) -> bool: return True diff --git a/custom_components/sat/mqtt/opentherm.py b/custom_components/sat/mqtt/opentherm.py index 215ea53d..4ac91cb4 100644 --- a/custom_components/sat/mqtt/opentherm.py +++ b/custom_components/sat/mqtt/opentherm.py @@ -30,6 +30,10 @@ class SatOpenThermMqttCoordinator(SatMqttCoordinator): """Class to manage to fetch data from the OTGW Gateway using mqtt.""" + @property + def device_type(self) -> str: + return "OpenThermGateway (via mqtt)" + @property def supports_setpoint_management(self): return True diff --git a/custom_components/sat/serial/__init__.py b/custom_components/sat/serial/__init__.py index 277b0d1f..5c6dd9d7 100644 --- a/custom_components/sat/serial/__init__.py +++ b/custom_components/sat/serial/__init__.py @@ -45,6 +45,10 @@ async def async_coroutine(event): def device_id(self) -> str: return self._port + @property + def device_type(self) -> str: + return "OpenThermGateway (via serial)" + @property def device_active(self) -> bool: return bool(self.get(DATA_MASTER_CH_ENABLED) or False) diff --git a/custom_components/sat/simulator/__init__.py b/custom_components/sat/simulator/__init__.py index a1c99df9..db8e7fd9 100644 --- a/custom_components/sat/simulator/__init__.py +++ b/custom_components/sat/simulator/__init__.py @@ -31,6 +31,10 @@ def __init__(self, hass: HomeAssistant, data: Mapping[str, Any], options: Mappin def device_id(self) -> str: return 'Simulator' + @property + def device_type(self) -> str: + return "Simulator" + @property def supports_setpoint_management(self) -> bool: return True diff --git a/custom_components/sat/switch/__init__.py b/custom_components/sat/switch/__init__.py index fb5fe67e..df8bcc8b 100644 --- a/custom_components/sat/switch/__init__.py +++ b/custom_components/sat/switch/__init__.py @@ -27,6 +27,10 @@ def __init__(self, hass: HomeAssistant, entity_id: str, data: Mapping[str, Any], def device_id(self) -> str: return self._entity.name + @property + def device_type(self) -> str: + return "Switch" + @property def setpoint(self) -> float: return self.minimum_setpoint From e26c7978d6b3b5f253a03ef3dfdeda08332eecdd Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 24 Dec 2024 23:51:24 +0100 Subject: [PATCH 195/213] Make sure we have a valid manufacturer --- custom_components/sat/entity.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_components/sat/entity.py b/custom_components/sat/entity.py index 9b0b4dd2..0e6a4963 100644 --- a/custom_components/sat/entity.py +++ b/custom_components/sat/entity.py @@ -25,11 +25,15 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn @property def device_info(self): + manufacturer = "Unknown" + if self._coordinator.manufacturer is not None: + manufacturer = self._coordinator.manufacturer.name + return DeviceInfo( name=NAME, + manufacturer=manufacturer, suggested_area="Living Room", model=self._coordinator.device_type, - manufacturer=self._coordinator.manufacturer.name, identifiers={(DOMAIN, self._config_entry.data.get(CONF_NAME))} ) From 98f90baa1140518aab7345ba40fbf6bc94a55ae7 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Thu, 26 Dec 2024 14:28:11 +0100 Subject: [PATCH 196/213] Fix relative modulation for EMS --- custom_components/sat/mqtt/ems.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/mqtt/ems.py b/custom_components/sat/mqtt/ems.py index 800ebafc..c7ad1f3d 100644 --- a/custom_components/sat/mqtt/ems.py +++ b/custom_components/sat/mqtt/ems.py @@ -127,7 +127,7 @@ async def async_set_heater_state(self, state: DeviceState) -> None: await super().async_set_heater_state(state) async def async_set_control_max_relative_modulation(self, value: int) -> None: - await self._publish_command(f'{{"cmd": "burnmaxpower", "value": {min(value, 20)}}}') + await self._publish_command(f'{{"cmd": "burnmaxpower", "value": {max(value, 20)}}}') await super().async_set_control_max_relative_modulation(value) From dee2353e1a0f57685b063e7d10751e9e19992c46 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 28 Dec 2024 20:54:45 +0100 Subject: [PATCH 197/213] Update README.md --- README.md | 393 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 228 insertions(+), 165 deletions(-) diff --git a/README.md b/README.md index 8e152127..e899a348 100644 --- a/README.md +++ b/README.md @@ -1,257 +1,320 @@ -# Smart Autotune Thermostat (SAT) +# Smart Autotune Thermostat [![hacs][hacs-badge]][hacs-url] [![release][release-badge]][release-url] ![build][build-badge] [![discord][discord-badge]][discord-url] - +**Please :star: this repo if you find it useful.** -![OpenTherm MQTT Integration](https://raw.githubusercontent.com/Alexwijn/SAT/develop/.github/images/opentherm-mqtt.png) -![Overshoot Protection Graph](https://raw.githubusercontent.com/Alexwijn/SAT/develop/.github/images/overshoot_protection.png) +**Your support for the countless hours we've dedicated to this development is greatly appreciated, though not required** -## Overview -The **Smart Autotune Thermostat (SAT)** is a custom component for [Home Assistant][home-assistant] designed to optimize your heating system's performance. It integrates with an [OpenTherm Gateway (OTGW)](https://otgw.tclcode.com/) via MQTT or Serial connection, and can also function as a PID ON/OFF thermostat. SAT provides advanced temperature control using Outside Temperature Compensation and Proportional-Integral-Derivative (PID) algorithms. Unlike other thermostat components, SAT supports automatic gain tuning and heating curve coefficients, allowing it to determine the optimal setpoint for your boiler without manual intervention. + -## Features +![opentherm-mqtt.png](https://raw.githubusercontent.com/Alexwijn/SAT/develop/.github/images/opentherm-mqtt.png) +![overshoot_protection.png](https://raw.githubusercontent.com/Alexwijn/SAT/develop/.github/images/overshoot_protection.png) -- Multi-room temperature control with support for temperature synchronization for main climates. -- Adjustable heating curve coefficients to fine-tune your heating system. -- Target temperature step for adjusting the temperature in smaller increments. -- Presets for different modes such as Away, Sleep, Home, Comfort. -- Automatic gains for PID control. -- PWM and automatic duty cycle. -- Climate valve offset to adjust the temperature reading for your climate valve. -- Sample time for PID control to fine-tune your system's response time. -- Open window detection. -### OpenTherm-Specific Features +## What is the Smart Autotune Thermostat? -- Overshoot protection value automatic calculation mechanism. -- Overshoot protection to prevent the boiler from overshooting the setpoint (Low-Load Control). -- Control Domestic Hot Water (DHW) setpoint. +The Smart Autotune Thermostat, or SAT for short, is a custom component for Home Assistant that seamlessly integrates with the following devices: +- [OpenTherm Gateway (OTGW)](https://otgw.tclcode.com/) (MQTT or Serial) +- [DIYLess](https://diyless.com/) Master OpenTherm Shield +- [Ihor Melnyk's](http://ihormelnyk.com/opentherm_adapter) OpenTherm adapter +- [Jiří Praus'](https://www.tindie.com/products/jiripraus/opentherm-gateway-arduino-shield/) OpenTherm Gateway Arduino Shield -## Installation +It can also function as a PID ON/OFF thermostat, providing advanced temperature control based on Outside Temperature compensation and the Proportional-Integral-Derivative (PID) algorithm. Unlike other thermostat components, SAT supports automatic gain tuning and heating curve coefficients. This capability allows it to determine the optimal setpoint for your boiler without any manual intervention. +## Features +OpenTherm ( MQTT / Serial / ESPHome ): +- Multi-room temperature control with support for temperature synchronization for main climates +- Overshoot protection value automatic calculation mechanism +- Adjustable heating curve coefficients to fine-tune your heating system +- Target temperature step for adjusting the temperature in smaller increments +- Presets for different modes such as Away, Sleep, Home, Comfort +- Automatic gains for PID control +- PWM and Automatic-duty cycle +- Overshoot protection to prevent the boiler from overshooting the setpoint ( Low-Load Control ) +- Climate valve offset to adjust the temperature reading for your climate valve +- Sample time for PID control to fine-tune your system's response time +- Open Window detection +- Control DHW setpoint + +PID ON/OFF thermostat: + +- Multi-room temperature control with support for temperature synchronization for main climates +- Adjustable heating curve coefficients to fine-tune your heating system +- Target temperature step for adjusting the temperature in smaller increments +- Presets for different modes such as Away, Sleep, Home, Comfort +- Automatic gains for PID control +- PWM and Automatic-duty cycle +- Climate valve offset to adjust the temperature reading for your climate valve +- Sample time for PID control to fine-tune your system's response time +- Open Window detection + +## Installation ### HACS -Smart Autotune Thermostat (SAT) is available in [HACS][hacs] (Home Assistant Community Store). +Smart Autotune Thermostat ( SAT ) is available in [HACS][hacs] (Home Assistant Community Store). -Use this link to directly go to the repository in HACS: +Use this link to directly go to the repository in HACS [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=Alexwijn&repository=SAT) -**Or follow these steps:** +_or_ -1. Install HACS if you don't have it already. -2. Open HACS in Home Assistant. -3. Search for **Smart Autotune Thermostat**. -4. Click the **Download** button. ⬇️ +1. Install HACS if you don't have it already +2. Open HACS in Home Assistant +3. Search for "Smart Autotune Thermostat" +4. Click the download button. ⬇️ -### Manual Installation - -1. Download the latest release of the SAT custom component from the [GitHub repository][release-url]. -2. Copy the `sat` directory to the `custom_components` directory in your Home Assistant configuration directory. If the `custom_components` directory doesn't exist, create it. +### Manual +1. Download the latest release of the SAT custom component from the GitHub repository. +2. Copy the sat directory to the custom_components directory in your Home Assistant configuration directory. If the custom_components directory doesn't exist, create it. 3. Restart Home Assistant to load the SAT custom component. 4. After installing the SAT custom component, you can configure it via the Home Assistant Config Flow interface. -## Configuration - -SAT is configured using a config flow. After installation, go to the **Integrations** page in Home Assistant, click on the **Add Integration** button, and search for **SAT** if the autodiscovery feature fails. - -### OpenTherm Configuration - -1. **OpenTherm Connection** - - - **MQTT**: - - **Name of the thermostat** - - **Top Topic** (*MQTT Top Topic* found in OTGW-firmware Settings) - - **Device** - - - **Serial**: - - **Name of the thermostat** - - **URL** - -2. **Configure Sensors** - - - **Inside Temperature Sensor** (Your room temperature sensor) - - **Outside Temperature Sensor** (Your outside temperature sensor) - - **Inside Humidity Sensor** (Your room humidity sensor) - -3. **Heating System** - - Selecting the correct heating system type is crucial for SAT to accurately control the temperature and optimize performance. Choose the option that matches your setup (e.g., Radiators or Underfloor heating) to ensure proper temperature regulation throughout your home. +# Configuration +SAT is configured using a config flow. After installation, go to the Integrations page in Home Assistant, click on the Add Integration button, and search for SAT if the autodiscovery feature fails. -4. **Multi-Room Setup** +## OpenTherm - > **Note:** If SAT is the only climate entity, skip this step. +1. OpenTherm Connection + - OpenTherm Gateway MQTT: + - Name of the thermostat + - Top Topic ( *MQTT Top Topic* found in OTGW-firmware Settings ) + - Device - - **Primary:** You can add your physical thermostat. SAT will synchronize the `hvac_action` of the physical thermostat with the SAT climate entity's `hvac_action`. Additionally, the physical thermostat will act as a backup if any failure to Home Assistant occurs. - - **Rooms:** You can add your TRV (Thermostatic Radiator Valve) climate entities. When any of the rooms request heating, SAT will start the boiler. + - OpenTherm Gateway Serial: + - Name of the thermostat + - URL - > **Tip:** Refer to the **Heating Mode** setting in the **General** tab for further customization. - -5. **Calibrate System** - - Optimize your heating system by automatically determining the optimal PID values for your setup. When selecting **Automatic Gains**, please note that the system will go through a calibration process that may take approximately 20 minutes to complete. - - If you already know this value, use the **Manually enter the overshoot protection value** option and enter the value. - - Automatic Gains are recommended for most users as it simplifies the setup process and ensures optimal performance. However, if you're familiar with PID control and prefer to manually set the values, you can choose to skip Automatic Gains. - - > **Note:** Choosing to skip Automatic Gains requires a good understanding of PID control and may require additional manual adjustments to achieve optimal performance. - -### PID ON/OFF Thermostat Configuration - -_To be completed._ - -## Settings - -### General Tab - -**Heating Curve Version** - -Represents the formulas used for calculating the heating curve. The available options are: - -- **Radiators**: - - [Classic Curve](https://www.desmos.com/calculator/cy8gjiciny) - - [Quantum Curve](https://www.desmos.com/calculator/hmrlrapnxz) - - [Precision Curve](https://www.desmos.com/calculator/spfvsid4ds) (**Recommended**) - -- **Underfloor Heating**: - - [Classic Curve](https://www.desmos.com/calculator/exjth5qsoe) - - [Quantum Curve](https://www.desmos.com/calculator/ke69ywalcz) - - [Precision Curve](https://www.desmos.com/calculator/i7f7uuyaoz) (**Recommended**) - -> **Note:** Graph parameters: -> -> - `a`: Heating Curve Coefficient -> - `b`: Room Setpoint - -> **Tip:** You can add the graph as an `iframe` card in Home Assistant for easy reference. - -**Example:** + - ESPHome Opentherm: + - Name of the thermostat + - Device +> [!Important] +> The ESPHome yaml needs to follow the exact naming of the following entities, otherwise SAT will not be able to find them in Home Assistant. +
+ESPHome minimal yaml configuration + ```yaml -type: iframe -url: https://www.desmos.com/calculator/spfvsid4ds -allow_open_top_navigation: true -allow: fullscreen -aspect_ratio: 130% +# Insert usual esphome configuration (board, api, ota, etc.) + +opentherm: + in_pin: # insert in pin + out_pin: # insert out pin + ch_enable: true + dhw_enable: true + +number: + - platform: opentherm + t_dhw_set: + name: t_dhw_set + step: 1 + restore_value: true + t_set: + name: t_set + restore_value: true + max_t_set: + name: max_t_set + step: 1 + restore_value: true + max_rel_mod_level: + name: max_rel_mod_level + min_value: 0 + max_value: 100 + step: 1 + initial_value: 100 + restore_value: true + +sensor: + - platform: opentherm + rel_mod_level: + name: rel_mod_level + device_id: + name: device_id + t_boiler: + name: t_boiler + t_ret: + name: t_ret + max_capacity: + name: max_capacity + min_mod_level: + name: min_mod_level + t_dhw_set_lb: + name: t_dhw_set_lb + t_dhw_set_ub: + name: t_dhw_set_ub + +binary_sensor: + - platform: opentherm + flame_on: + name: flame_on + +switch: + - platform: opentherm + dhw_enable: + name: dhw_enable + ch_enable: + name: ch_enable ``` -**PID Controller Version** - -- **Classic Controller** -- **Improved Controller** +For more information about which other entities are available for OpenTherm please visit the [ESPHome OpenTherm documentation](https://esphome.io/components/opentherm.html) -**Heating Mode** +
-> **Note:** Available only for multi-room installations. +2. Configure sensors: + - Inside Temperature sensor ( Your Room Temperature sensor ) + - Outside Temperature sensor ( Your Outside Temperature sensor ) + - Inside Humidity Sensor ( Your Room Humidity sensor ) -- **Comfort:** SAT monitors the climates in other rooms to determine the error. It selects the highest error value as the PID error value for the current room. -- **Eco:** SAT monitors **only** the main thermostat's error, which is used as the PID error. +> [!NOTE] +> For better results use an Inside Temperature sensor that reports two decimals and has a refresh rate of 30 seconds. -**Maximum Setpoint** +3. Heating System: Selecting the correct heating system type is important for SAT to accurately control the temperature and optimize performance. Choose the option that matches your setup to ensure proper temperature regulation throughout your home. -Set the maximum water setpoint for your system. +4. Areas: + - Primary: Users can add their physical thermostat. SAT will syncronize the `hvac_action` of the physical thermostat with the SAT climate entity's `hvac action`, that means if the physical thermostat doesn't require heating then the SAT climate entity `hvac_action` will remain at idle. Also the physical thermostat's room setpoint stays in sync with SAT climate entity. Moreover the physical thermostat will act as a back up if any failure to HA occurs. + - Rooms: Users can add their TRV climate entities. So when any of the rooms will ask for heating, SAT will start the boiler. +> [!Note] +> If SAT is the only climate entity, skip this step. -- **Radiators:** Recommended to choose a value between 55–75 °C. Higher values will cause a more aggressive warm-up. -- **Underfloor Heating:** Recommended maximum water setpoint is 50 °C. +> [!TIP] +> Look at the Heating Mode setting in General Tab for further customization. -**Heating Curve Coefficient** +5. Calibrate System: Optimize your heating system by automatically determining the optimal PID values for your setup. When selecting Automatic Gains, please note that the system will go through a calibration process that may take approximately 20 minutes to complete. -Adjust the heating curve coefficient to balance the heating loss of your home with the energy generated from your boiler based on the outside temperature. Proper tuning ensures the room temperature hovers around the setpoint. +If you already know this value, then use the "Manually enter the overshoot protection value" option and fill the value. -**Automatic Gains Value** +Automatic Gains are recommended for most users as it simplifies the setup process and ensures optimal performance. However, if you're familiar with PID control and prefer to manually set the values, you can choose to skip Automatic Gains. -Automatically tweak the aggressiveness of the PID gains (`kP`, `kI`, and `kD` values). Best results are achieved when using the same value as the Heating Curve Coefficient. +Please note that choosing to skip Automatic Gains requires a good understanding of PID control and may require additional manual adjustments to achieve optimal performance. -**Derivative Time Weight** +## PID ON/OFF -Further tweak the `kD` value. A good starting value is `2`. +To be completed -**Adjustment Factor for Return Temperature** +# Configure -This factor adjusts the heating setpoint based on the boiler's return temperature, affecting heating responsiveness and efficiency. A higher value increases sensitivity to temperature changes, enhancing control over comfort and energy use. +## General tab: +*Heating Curve Version*: Represents the 3 formulas of calculation. The available options are: -> **Tip:** Recommended starting range is `0.1` to `0.5`. Adjust to suit your system and comfort preferences. +Radiators: +- [Classic Curve](https://www.desmos.com/calculator/cy8gjiciny) +- [Quantum Curve](https://www.desmos.com/calculator/hmrlrapnxz) +- [Precision Curve](https://www.desmos.com/calculator/spfvsid4ds) ( Recommented ) -**Contact Sensor** +Underfloor: +- [Classic Curve](https://www.desmos.com/calculator/exjth5qsoe) +- [Quantum Curve](https://www.desmos.com/calculator/ke69ywalcz) +- [Precision Curve](https://www.desmos.com/calculator/i7f7uuyaoz) ( Recommented ) -Add contact sensors (e.g., door/window sensors) to avoid wasting energy when a door/window is open. When the door/window is closed again, SAT restores heating. +> [!NOTE] +> Graph parameters: +> - a: Heating Curve Value +> - b: Room Setpoint -### Presets Tab +> [!TIP] +> You can add the graph as an `iframe` card in HA. +> +> Example: +> ```yaml +> type: iframe +> url: https://www.desmos.com/calculator/spfvsid4ds +> allow_open_top_navigation: true +> allow: fullscreen +> aspect_ratio: 130% -Predefined temperature settings for different scenarios or activities, such as Away, Sleep, Home, and Comfort. +*PID Controller Version*: +- Classic Controller +- Improved Controller -### Advanced Tab +*Heating Mode*: -**Thermal Comfort** +> [!NOTE] +>Available only for multiroom installations -Uses the Summer Simmer Index as the temperature sensor. The Summer Simmer Index refers to the perceived temperature based on the measured air temperature and relative humidity. +- Comfort ( SAT monitors the climates in other rooms to determine the error. It selects the highest error value as the PID error value for the current room ) +- Eco ( SAT monitors **only** the Main thermostat's error and it is used as the PID error ) -**Dynamic Minimum Setpoint (Experimental)** +*Maximum Setpoint*: +You can choose the max water setpoint for your system. +For radiator installations, it is recommended to choose a value between 55-75 °C. +For underfloor installations, the recommended max water setpoint is 50 °C. -In multi-room installations, the boiler flow water temperature may exceed the Overshoot Protection Value during Low-Load Control (some valves may be closed). This mechanism monitors the boiler return water temperature and adjusts the Control Setpoint sent to the boiler accordingly. See **Adjustment Factor for Return Temperature**. +> [!NOTE] +> Radiators: Higher Max water setpoint values will cause a more aggressive warm-up. -**Minimum Consumption** +*Heating Curve Coefficient*: +The heating curve coefficient is a configurable parameter in SAT that allows you to adjust the relationship between the outdoor temperature and the heating system output. This is useful for optimizing the heating system's performance in different weather conditions, as it allows you to adjust how much heat the system delivers as the outdoor temperature changes. By tweaking this parameter, you can achieve a more efficient and comfortable heating system. -Find this value in your boiler's manual. SAT uses this value to calculate the instant gas consumption. +*Automatic Gains Value*: Automatically tweaking the aggressiveness of the Kp, Ki and Kd gains. -**Maximum Consumption** +> [!TIP] +> Best results when the user uses the same value as the Heating Curve Coefficient value. -Find this value in your boiler's manual. SAT uses this value to calculate the instant gas consumption. +*Derivative Time Weight*: Further tweaking of the Kd value. -**Target Temperature Step** +> [!TIP] +> Better start with the value `2`. -Adjusts the SAT climate entity room setpoint step. +*Adjustment Factor for Return Temperature*: +This factor adjusts the heating setpoint based on the boiler's return temperature, affecting heating responsiveness and efficiency. A higher value increases sensitivity to temperature changes, enhancing control over comfort and energy use. -**Maximum Relative Modulation** +> [!TIP] +> Recommended starting range is 0.1 to 0.5. Adjust to suit your system and comfort preferences. -Control the maximum relative modulation at which the boiler will operate. +*Contact Sensor*: You can add contact sensors to avoid wasting energy when a door/window is open. When the door/window is closed again, SAT restores heating. -## Terminology +## Presets tab: +Predefined temperature settings for different scenarios or activities. -**Heating Curve Coefficient** +## Advanced Tab +*Thermal Comfort*: Uses as temperature sensor the Summer SImmer Index. The Summer Simmer Index refers to the perceived temperature based on the measured air temperature and relative humidity. -By adjusting the heating curve coefficient, you can balance the heating loss of your home with the energy generated from your boiler at a given setpoint based on the outside temperature. Proper tuning ensures the room temperature hovers around the setpoint. + *Dynamic Minimum Setpoint (Experimental)*: The Boiler flow water temperature may exceed the Overshoot Protection Value during Low-Load Control in multiroom installations ( Some valves may be closed ). We developed a mechanishm that monitors the boiler return water temperature and changes accordingly the Control Setpoint that is sent to the boiler. See *Adjustment Factor for Return Temperature*. -**PID Gains** +*Minimum Consumption*: The user can find this value at the boiler's manual. SAT uses this value in order to calculate the instant gas consumption. -SAT offers two ways of tuning the PID gains: +*Maximum Consumption*: The user can find this value at the boiler's manual. SAT uses this value in order to calculate the instant gas consumption. -- **Manual Tuning:** Fill the Proportional (`kP`), Integral (`kI`), and Derivative (`kD`) fields in the General tab with your values. -- **Automatic Gains (Recommended):** Enabled by default when the Overshoot Protection Value is present (during initial configuration). Automatic gains dynamically change the `kP`, `kI`, and `kD` values based on the heating curve value. This means that, based on the outside temperature, the gains change from mild to aggressive without intervention. +*Target Temperature Step*: SAT climate entity room setpoint step. -**Overshoot Protection** +*Maximum Relative Modulation*: The user is able to control the maximum relative modulation that the boiler will operate. -This feature should be enabled when the minimum boiler capacity is greater than the control setpoint calculated by SAT. If the boiler overshoots the control setpoint, it may cycle, shortening the life of the burner. The solution is to adjust the boiler's on/off times to maintain the temperature at the setpoint while minimizing cycling. +# Terminology +*Heating Curve Coefficient*: By adjusting the heating curve coefficient, you can balance the heating loss of your home with the energy generated from your boiler at a given setpoint based on the outside temperature. When this value is properly tuned, the room temperature should hover around the setpoint. -**Overshoot Protection Value (OPV) Calculation** +*Gains*: SAT offers two ways of tuning the PID gains - manual and automatic. -The OPV is a crucial value that determines the boiler's on/off times when the Overshoot Protection feature is present (during initial configuration). +- Manual tuning: You can fill the Proportional, Integral, and Derivative fields in the General tab with your values. +- Automatic Gains ( Recommended ): This option is enabled by default when the Overshoot protection value is present (During initial configuration). Automatic gains dynamically change the kP, kI, and kD values based on the heating curve value. So, based on the outside temperature, the gains change from mild to aggressive without intervention. -- **Automatic Calculation:** To calculate the OPV automatically, choose the **Calibrate and determine your overshoot protection value (approx. 20 min)** option during the initial configuration. SAT will then send the `MM=0` and `CS=75` commands, attempting to find the highest flow water temperature the boiler can produce while running at 0% modulation. This process takes at least 20 minutes. Once the OPV calculation is complete, SAT will resume normal operation and send a completion notification. The calculated value will be stored as an attribute in the SAT climate entity and used to determine the boiler's on/off times in the low-load control algorithm. If SAT detects that the boiler doesn't respect the 0% Max Modulation command, it will automatically change the calibration algorithm to a more sophisticated one to perform the calibration of the system. +*Overshoot Protection*: This feature should be enabled when the minimum boiler capacity is greater than the control setpoint calculated by SAT. If the boiler overshoots the control setpoint, it may cycle, shortening the life of the burner. The solution is to adjust the boiler's on/off times to maintain the temperature at the setpoint while minimizing cycling. -- **Manual Calculation:** If you know the maximum flow water temperature of the boiler at 0% modulation, you can fill in this value during the initial configuration. +*Overshoot Protection Value (OPV) Calculation*: +The OPV is a crucial value that determines the boiler's on/off times when the Overshoot Protection feature is present (During initial configuration). -> **Note:** If you have any TRVs, open all of them (set them to a high setpoint) to ensure accurate calculation of the OPV. Once the calculation is complete, you can lower the setpoint back to your desired temperature. +*Automatic Calculation*: To calculate the OPV automatically, choose the "Calibrate and determine your overshoot protection value (approx. 20 min)" option during the initial configuration. SAT will then send the MM=0 and CS=75 commands, attempting to find the highest flow water temperature the boiler can produce while running at 0% modulation. This process takes at least 20 minutes. Once the OPV calculation is complete, SAT will resume normal operation and send a completion notification. The calculated value will be stored as an attribute in the SAT climate entity and used to determine the boiler's on/off times in the low-load control algorithm. If SAT detects that the boiler doesn't respect the 0% Max Modulation command, it will automatically change the calibration algorithm to a more sophisticated one to perform the calibration of the system. -**Automatic Duty Cycle** +*Manual Calculation*: If you know the maximum flow water temperature of the boiler at 0% modulation, you can fill in this value during the initial configuration. -When this option is enabled, SAT calculates the ON and OFF times of the boiler in 15-minute intervals, given that the kW needed to heat the home is less than the minimum boiler capacity. Additionally, using this feature, SAT can efficiently regulate the room temperature even in mild weather by automatically extending the duty cycle up to 30 minutes. +> [!Note] +> If you have any TRVs, open all of them (set them to a high setpoint) to ensure accurate calculation of the OPV. Once the calculation is complete, you can lower the setpoint back to your desired temperature. -> **Tip:** For a more in-depth review of SAT and real-time observations, you can read this [excellent discussion post](https://github.com/Alexwijn/SAT/discussions/40) from [@critictidier](https://github.com/critictidier). +*Automatic Duty Cycle*: When this option is enabled, SAT calculates the ON and OFF times of the boiler in 15-minute intervals, given that the kW needed to heat the home is less than the minimum boiler capacity. Moreover, using this feature, SAT can efficiently regulate the room temperature even in mild weather by automatically extending the duty cycle up to 30 minutes. ---- +> [!TIP] +> For more in depth review of SAT and real time observations you can read this [excellent discussion post](https://github.com/Alexwijn/SAT/discussions/40) from @critictidier. [hacs-url]: https://github.com/hacs/integration [hacs-badge]: https://img.shields.io/badge/hacs-default-orange.svg?style=for-the-badge [release-badge]: https://img.shields.io/github/v/tag/Alexwijn/SAT?style=for-the-badge +[downloads-badge]: https://img.shields.io/github/downloads/Alexwijn/SAT/total?style=for-the-badge [build-badge]: https://img.shields.io/github/actions/workflow/status/Alexwijn/SAT/pytest.yml?branch=develop&style=for-the-badge [discord-badge]: https://img.shields.io/discord/1184879273991995515?label=Discord&logo=discord&logoColor=white&style=for-the-badge From 7db0f48f44f370bb8cbd2ddf481299f2f25c135e Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 28 Dec 2024 20:54:59 +0100 Subject: [PATCH 198/213] Add support for Optimized PID Controller --- custom_components/sat/config_flow.py | 16 +++++++++------- custom_components/sat/const.py | 2 +- custom_components/sat/pid.py | 21 ++++++++++++++++++--- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index a40be06a..1eb21a8c 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -537,7 +537,8 @@ async def async_step_general(self, _user_input: dict[str, Any] | None = None): schema[vol.Required(CONF_PID_CONTROLLER_VERSION, default=str(options[CONF_PID_CONTROLLER_VERSION]))] = selector.SelectSelector( selector.SelectSelectorConfig(mode=SelectSelectorMode.DROPDOWN, options=[ selector.SelectOptionDict(value="1", label="Classic Controller"), - selector.SelectOptionDict(value="2", label="Improved Controller") + selector.SelectOptionDict(value="2", label="Improved Controller"), + selector.SelectOptionDict(value="3", label="Optimized Controller") ]) ) @@ -558,12 +559,13 @@ async def async_step_general(self, _user_input: dict[str, Any] | None = None): ) if options[CONF_AUTOMATIC_GAINS]: - schema[vol.Required(CONF_AUTOMATIC_GAINS_VALUE, default=options[CONF_AUTOMATIC_GAINS_VALUE])] = selector.NumberSelector( - selector.NumberSelectorConfig(min=1, max=5, step=0.1) - ) - schema[vol.Required(CONF_DERIVATIVE_TIME_WEIGHT, default=options[CONF_DERIVATIVE_TIME_WEIGHT])] = selector.NumberSelector( - selector.NumberSelectorConfig(min=1, max=6, step=0.1) - ) + if int(options[CONF_PID_CONTROLLER_VERSION]) < 3: + schema[vol.Required(CONF_AUTOMATIC_GAINS_VALUE, default=options[CONF_AUTOMATIC_GAINS_VALUE])] = selector.NumberSelector( + selector.NumberSelectorConfig(min=1, max=5, step=0.1) + ) + schema[vol.Required(CONF_DERIVATIVE_TIME_WEIGHT, default=options[CONF_DERIVATIVE_TIME_WEIGHT])] = selector.NumberSelector( + selector.NumberSelectorConfig(min=1, max=6, step=0.1) + ) else: schema[vol.Required(CONF_PROPORTIONAL, default=options[CONF_PROPORTIONAL])] = str schema[vol.Required(CONF_INTEGRAL, default=options[CONF_INTEGRAL])] = str diff --git a/custom_components/sat/const.py b/custom_components/sat/const.py index c786e33f..e6f97610 100644 --- a/custom_components/sat/const.py +++ b/custom_components/sat/const.py @@ -144,7 +144,7 @@ CONF_HEATING_MODE: HEATING_MODE_COMFORT, CONF_HEATING_SYSTEM: HEATING_SYSTEM_RADIATORS, - CONF_PID_CONTROLLER_VERSION: 2, + CONF_PID_CONTROLLER_VERSION: 3, } # Overshoot protection diff --git a/custom_components/sat/pid.py b/custom_components/sat/pid.py index c96a33ed..eca7dbe2 100644 --- a/custom_components/sat/pid.py +++ b/custom_components/sat/pid.py @@ -18,6 +18,7 @@ class PID: def __init__(self, heating_system: str, automatic_gain_value: float, + heating_curve_coefficient: float, derivative_time_weight: float, kp: float, ki: float, kd: float, max_history: int = 2, @@ -25,13 +26,13 @@ def __init__(self, automatic_gains: bool = False, integral_time_limit: float = 300, sample_time_limit: Optional[float] = 10, - version: int = 2): + version: int = 3): """ Initialize the PID controller. :param heating_system: The type of heating system, either "underfloor" or "radiator" - :param automatic_gain_value: The value to finetune the aggression value. - :param derivative_time_weight: The weight to finetune the derivative. + :param automatic_gain_value: The value to fine-tune the aggression value. + :param derivative_time_weight: The weight to fine-tune the derivative. :param kp: The proportional gain of the PID controller. :param ki: The integral gain of the PID controller. :param kd: The derivative gain of the PID controller. @@ -51,6 +52,7 @@ def __init__(self, self._automatic_gains = automatic_gains self._automatic_gains_value = automatic_gain_value self._derivative_time_weight = derivative_time_weight + self._heating_curve_coefficient = heating_curve_coefficient self._last_interval_updated = monotonic() self._sample_time_limit = max(sample_time_limit, 1) @@ -259,6 +261,9 @@ def _get_aggression_value(self): if self._version == 2: return 73 if self._heating_system == HEATING_SYSTEM_UNDERFLOOR else 81.5 + if self._version == 3: + return 8400 + raise Exception("Invalid version") @property @@ -283,6 +288,10 @@ def last_updated(self) -> float: def kp(self) -> float | None: """Return the value of kp based on the current configuration.""" if self._automatic_gains: + if self._version == 3: + automatic_gain_value = 4 if self._heating_system == HEATING_SYSTEM_UNDERFLOOR else 3 + return round((self._heating_curve_coefficient * self._last_heating_curve_value) / automatic_gain_value, 6) + automatic_gain_value = 0.243 if self._heating_system == HEATING_SYSTEM_UNDERFLOOR else 0.33 return round(self._automatic_gains_value * automatic_gain_value * self._last_heating_curve_value, 6) @@ -301,6 +310,9 @@ def ki(self) -> float | None: if self._version == 2: return round(self._automatic_gains_value * (self._last_heating_curve_value / 7200), 6) + if self._version == 3: + return round(self.kp / self._get_aggression_value(), 6) + raise Exception("Invalid version") return float(self._ki) @@ -312,6 +324,9 @@ def kd(self) -> float | None: if self._last_heating_curve_value is None: return 0 + if self._version == 3: + return round(0.07 * self._get_aggression_value() * self.kp, 6) + return round(self._automatic_gains_value * self._get_aggression_value() * self._derivative_time_weight * self._last_heating_curve_value, 6) return float(self._kd) From 8cc7449881609039904a41be373c27dde51f803f Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 28 Dec 2024 20:56:28 +0100 Subject: [PATCH 199/213] Update naming --- custom_components/sat/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 1eb21a8c..760aa7eb 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -538,7 +538,7 @@ async def async_step_general(self, _user_input: dict[str, Any] | None = None): selector.SelectSelectorConfig(mode=SelectSelectorMode.DROPDOWN, options=[ selector.SelectOptionDict(value="1", label="Classic Controller"), selector.SelectOptionDict(value="2", label="Improved Controller"), - selector.SelectOptionDict(value="3", label="Optimized Controller") + selector.SelectOptionDict(value="3", label="Adaptive Controller") ]) ) From fb1ef39929204dd0533b6ac5a144793532dd6596 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 28 Dec 2024 20:57:18 +0100 Subject: [PATCH 200/213] Add missing parameter --- custom_components/sat/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/sat/util.py b/custom_components/sat/util.py index 714212a1..d45e3e6e 100644 --- a/custom_components/sat/util.py +++ b/custom_components/sat/util.py @@ -53,6 +53,7 @@ def create_pid_controller(config_options) -> PID: automatic_gains = bool(config_options.get(CONF_AUTOMATIC_GAINS)) automatic_gains_value = float(config_options.get(CONF_AUTOMATIC_GAINS_VALUE)) derivative_time_weight = float(config_options.get(CONF_DERIVATIVE_TIME_WEIGHT)) + heating_curve_coefficient = float(config_options.get(CONF_HEATING_CURVE_COEFFICIENT)) sample_time_limit = convert_time_str_to_seconds(config_options.get(CONF_SAMPLE_TIME)) # Return a new PID controller instance with the given configuration options @@ -61,6 +62,7 @@ def create_pid_controller(config_options) -> PID: heating_system=heating_system, automatic_gain_value=automatic_gains_value, derivative_time_weight=derivative_time_weight, + heating_curve_coefficient=heating_curve_coefficient, kp=kp, ki=ki, kd=kd, automatic_gains=automatic_gains, From 8fcf31e020ee227d9d905583073657b87c01e131 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 29 Dec 2024 14:14:23 +0100 Subject: [PATCH 201/213] Use latest version of pyotgw --- custom_components/sat/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/manifest.json b/custom_components/sat/manifest.json index 61e532c0..d93de40e 100644 --- a/custom_components/sat/manifest.json +++ b/custom_components/sat/manifest.json @@ -22,7 +22,7 @@ "OTGW/value/+" ], "requirements": [ - "pyotgw==2.1.3", + "pyotgw==2.2.2", "sentry-sdk==2.19.2" ], "version": "4.0.0" From 3be949666930ca429047567442dded206d119588 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sun, 29 Dec 2024 14:17:56 +0100 Subject: [PATCH 202/213] Cleaning --- custom_components/sat/esphome/__init__.py | 5 +---- custom_components/sat/serial/__init__.py | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/custom_components/sat/esphome/__init__.py b/custom_components/sat/esphome/__init__.py index d6370151..2a8585f6 100644 --- a/custom_components/sat/esphome/__init__.py +++ b/custom_components/sat/esphome/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations, annotations import logging -from typing import TYPE_CHECKING, Mapping, Any +from typing import Mapping, Any from homeassistant.components import mqtt from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -38,9 +38,6 @@ DATA_MAX_CH_SETPOINT = "max_t_set" DATA_MAX_REL_MOD_LEVEL_SETTING = "max_rel_mod_level" -if TYPE_CHECKING: - pass - _LOGGER: logging.Logger = logging.getLogger(__name__) diff --git a/custom_components/sat/serial/__init__.py b/custom_components/sat/serial/__init__.py index 5c6dd9d7..e1b3f4b8 100644 --- a/custom_components/sat/serial/__init__.py +++ b/custom_components/sat/serial/__init__.py @@ -2,7 +2,7 @@ import asyncio import logging -from typing import Optional, Any, TYPE_CHECKING, Mapping +from typing import Optional, Any, Mapping from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -12,9 +12,6 @@ from ..coordinator import DeviceState, SatDataUpdateCoordinator -if TYPE_CHECKING: - pass - _LOGGER: logging.Logger = logging.getLogger(__name__) # Sensors From 3dd9b10c618b1845ce16fdffe12e861b1154ccc9 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds <1023654+Alexwijn@users.noreply.github.com> Date: Sun, 29 Dec 2024 15:06:03 +0100 Subject: [PATCH 203/213] Update custom_components/sat/translations/sk.json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- custom_components/sat/translations/sk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/translations/sk.json b/custom_components/sat/translations/sk.json index 3579b7e7..4ede0c46 100644 --- a/custom_components/sat/translations/sk.json +++ b/custom_components/sat/translations/sk.json @@ -113,7 +113,7 @@ "data": { "maximum_setpoint": "Maximálna požadovaná hodnota", "minimum_setpoint": "Minimálna požadovaná hodnota", - "name": "Name", + "name": "Názov", "simulated_cooling": "Simulované chladenie", "simulated_heating": "Simulovaný ohrev", "simulated_warming_up": "Simulované zahrievanie" From 033cc51c7ad49d9ed4a1df9c264e5c5c2b7addd4 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds <1023654+Alexwijn@users.noreply.github.com> Date: Sun, 29 Dec 2024 15:06:25 +0100 Subject: [PATCH 204/213] Update custom_components/sat/translations/pt.json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- custom_components/sat/translations/pt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/translations/pt.json b/custom_components/sat/translations/pt.json index 9034259f..8d969913 100644 --- a/custom_components/sat/translations/pt.json +++ b/custom_components/sat/translations/pt.json @@ -116,7 +116,7 @@ "name": "Nome", "simulated_cooling": "Arrefecimento Simulado", "simulated_heating": "Aquecimento Simulado", - "simulated_warming_up": "Aquecimento Simulado" + "simulated_warming_up": "Aquecimento Inicial Simulado" }, "description": "Este gateway permite-lhe simular uma caldeira para fins de teste e demonstração. Por favor forneça as seguintes informações para configurar o simulador.\n\nNota: O Simulador Gateway destina-se apenas a fins de teste e demonstração e não deve ser usado em ambientes de produção.", "title": "Gateway Simulado ( AVANÇADO )" From 821607eecadb280a92fb09ac02e65fb052de2ec0 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 31 Dec 2024 14:42:17 +0100 Subject: [PATCH 205/213] Improve coordinator setup process --- custom_components/sat/__init__.py | 2 +- custom_components/sat/coordinator.py | 2 +- custom_components/sat/mqtt/__init__.py | 2 +- custom_components/sat/serial/__init__.py | 4 +--- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index 999a806e..b05aed98 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) # Making sure everything is loaded - await hass.data[DOMAIN][entry.entry_id][COORDINATOR].setup() + await hass.data[DOMAIN][entry.entry_id][COORDINATOR].async_setup() # Forward entry setup for used platforms await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/custom_components/sat/coordinator.py b/custom_components/sat/coordinator.py index 94f1e7d7..ac104ccf 100644 --- a/custom_components/sat/coordinator.py +++ b/custom_components/sat/coordinator.py @@ -253,7 +253,7 @@ def supports_maximum_setpoint_management(self): """ return False - async def setup(self) -> None: + async def async_setup(self) -> None: """Perform setup when the integration is about to be added to Home Assistant.""" pass diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 65c7ef33..7a90fc3f 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -31,7 +31,7 @@ def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], def device_id(self) -> str: return self._device_id - async def setup(self): + async def async_setup(self): await self._load_stored_data() async def async_added_to_hass(self) -> None: diff --git a/custom_components/sat/serial/__init__.py b/custom_components/sat/serial/__init__.py index e1b3f4b8..856875ea 100644 --- a/custom_components/sat/serial/__init__.py +++ b/custom_components/sat/serial/__init__.py @@ -167,11 +167,9 @@ async def async_connect(self) -> SatSerialCoordinator: return self - async def async_added_to_hass(self) -> None: + async def async_setup(self) -> None: await self.async_connect() - await super().async_added_to_hass() - async def async_will_remove_from_hass(self) -> None: self._api.unsubscribe(self.async_set_updated_data) From edc6c373ff56b32527fb8e030c5d24660ee154b3 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 31 Dec 2024 14:46:28 +0100 Subject: [PATCH 206/213] Cleaning things up --- custom_components/sat/__init__.py | 14 +++++--- custom_components/sat/config_flow.py | 2 +- custom_components/sat/mqtt/__init__.py | 2 +- custom_components/sat/relative_modulation.py | 35 ++++++-------------- 4 files changed, 21 insertions(+), 32 deletions(-) diff --git a/custom_components/sat/__init__.py b/custom_components/sat/__init__.py index b05aed98..76589991 100644 --- a/custom_components/sat/__init__.py +++ b/custom_components/sat/__init__.py @@ -19,7 +19,7 @@ COORDINATOR, CONF_MODE, CONF_DEVICE, - CONF_ERROR_MONITORING, + CONF_ERROR_MONITORING, OPTIONS_DEFAULTS, ) from .coordinator import SatDataUpdateCoordinatorFactory @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = {} # Setup error monitoring (if enabled) - if entry.options.get(CONF_ERROR_MONITORING, True): + if entry.options.get(CONF_ERROR_MONITORING, OPTIONS_DEFAULTS[CONF_ERROR_MONITORING]): await hass.async_add_executor_job(initialize_sentry, hass) # Resolve the coordinator by using the factory according to the mode @@ -77,9 +77,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.gather(hass.config_entries.async_unload_platforms(entry, PLATFORMS)) ) - if SENTRY in hass.data[DOMAIN]: - hass.data[DOMAIN][SENTRY].flush() - hass.data[DOMAIN][SENTRY].close() + try: + if SENTRY in hass.data[DOMAIN]: + hass.data[DOMAIN][SENTRY].flush() + hass.data[DOMAIN][SENTRY].close() + hass.data[DOMAIN].pop(SENTRY, None) + except Exception as ex: + _LOGGER.error("Error during Sentry cleanup: %s", str(ex)) # Remove the entry from the data dictionary if all components are unloaded successfully if unloaded: diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index 760aa7eb..4bf9979e 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -670,7 +670,7 @@ async def async_step_advanced(self, _user_input: dict[str, Any] | None = None): vol.Required(CONF_DYNAMIC_MINIMUM_SETPOINT, default=options[CONF_DYNAMIC_MINIMUM_SETPOINT]): bool, } - if options.get(CONF_MODE) in [MODE_MQTT_OPENTHERM, MODE_SERIAL, MODE_SIMULATOR]: + if self._config_entry.data.get(CONF_MODE) in [MODE_MQTT_OPENTHERM, MODE_SERIAL, MODE_SIMULATOR]: schema[vol.Required(CONF_FORCE_PULSE_WIDTH_MODULATION, default=options[CONF_FORCE_PULSE_WIDTH_MODULATION])] = bool schema[vol.Required(CONF_MINIMUM_CONSUMPTION, default=options[CONF_MINIMUM_CONSUMPTION])] = selector.NumberSelector( diff --git a/custom_components/sat/mqtt/__init__.py b/custom_components/sat/mqtt/__init__.py index 7a90fc3f..52a8e03e 100644 --- a/custom_components/sat/mqtt/__init__.py +++ b/custom_components/sat/mqtt/__init__.py @@ -112,7 +112,7 @@ def _process_message_payload(self, key: str, payload): """Process and store the payload of a received MQTT message.""" self.data[key] = payload - async def _publish_command(self, payload: str, wait_time: float = 0.5): + async def _publish_command(self, payload: str, wait_time: float = 1.0): """Publish a command to the MQTT topic.""" topic = self._get_topic_for_publishing() diff --git a/custom_components/sat/relative_modulation.py b/custom_components/sat/relative_modulation.py index 3a979dec..1b2d2656 100644 --- a/custom_components/sat/relative_modulation.py +++ b/custom_components/sat/relative_modulation.py @@ -1,9 +1,8 @@ import logging from enum import Enum -from .const import MINIMUM_SETPOINT, HEATING_SYSTEM_HEAT_PUMP +from .const import MINIMUM_SETPOINT from .coordinator import SatDataUpdateCoordinator -from .pwm import PWMState _LOGGER = logging.getLogger(__name__) @@ -13,51 +12,37 @@ class RelativeModulationState(str, Enum): OFF = "off" COLD = "cold" HOT_WATER = "hot_water" - WARMING_UP = "warming_up" PULSE_WIDTH_MODULATION_OFF = "pulse_width_modulation_off" class RelativeModulation: def __init__(self, coordinator: SatDataUpdateCoordinator, heating_system: str): """Initialize instance variables""" - self._heating_system = heating_system - self._pwm_state = None - self._warming_up = False self._coordinator = coordinator + self._heating_system = heating_system + self._pulse_width_modulation_enabled = None _LOGGER.debug("Relative Modulation initialized for heating system: %s", heating_system) - async def update(self, warming_up: bool, state: PWMState) -> None: - """Update internal state with new data received from the coordinator""" - self._pwm_state = state - self._warming_up = warming_up - - _LOGGER.debug("Updated Relative Modulation: enabled=%s, state=%s", self.enabled, self.state) + async def update(self, pulse_width_modulation_enabled: bool) -> None: + """Update internal state with new internal data""" + self._pulse_width_modulation_enabled = pulse_width_modulation_enabled @property def state(self) -> RelativeModulationState: """Determine the current state of relative modulation based on coordinator and internal data""" - # If setpoint is not available or below the minimum threshold, it's considered COLD - if self._coordinator.setpoint is None or self._coordinator.setpoint <= MINIMUM_SETPOINT: - return RelativeModulationState.COLD - - # If hot water is actively being used, it's considered HOT_WATER if self._coordinator.hot_water_active: return RelativeModulationState.HOT_WATER - # If the heating system is currently in the process of warming up, it's considered WARMING_UP - if self._warming_up and self._heating_system != HEATING_SYSTEM_HEAT_PUMP: - return RelativeModulationState.WARMING_UP + if not self._pulse_width_modulation_enabled: + if self._coordinator.setpoint is None or self._coordinator.setpoint <= MINIMUM_SETPOINT: + return RelativeModulationState.COLD - # If the PWM state is in the ON state, it's considered PULSE_WIDTH_MODULATION_OFF - if self._pwm_state != PWMState.ON: return RelativeModulationState.PULSE_WIDTH_MODULATION_OFF - # Default case, when none of the above conditions are met, it's considered OFF return RelativeModulationState.OFF @property def enabled(self) -> bool: """Check if the relative modulation is enabled based on its current state""" - # Relative modulation is considered enabled if it's not in the OFF state or in the WARMING_UP state - return self.state != RelativeModulationState.OFF and self.state != RelativeModulationState.WARMING_UP + return self.state != RelativeModulationState.OFF \ No newline at end of file From 783a8dab85b331114d804a85df4dfb03fc8f2a92 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 31 Dec 2024 15:14:03 +0100 Subject: [PATCH 207/213] Typo? --- custom_components/sat/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 22c254a5..9c36b518 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -806,7 +806,7 @@ async def _async_control_relative_modulation(self) -> None: return # Update relative modulation state - await self._relative_modulation.update(self.warming_up, self.pwm.state) + await self._relative_modulation.update(self.pulse_width_modulation_enabled) # Determine if the value needs to be updated if self._coordinator.maximum_relative_modulation_value == self.relative_modulation_value: From ac504a2b19fac4aeac78d9dbcddca24e4342e697 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 31 Dec 2024 16:38:42 +0100 Subject: [PATCH 208/213] Do not check deadband in PWM --- custom_components/sat/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/climate.py b/custom_components/sat/climate.py index 9c36b518..b837e718 100644 --- a/custom_components/sat/climate.py +++ b/custom_components/sat/climate.py @@ -788,7 +788,7 @@ async def _async_control_setpoint(self, pwm_state: PWMState) -> None: # PWM is enabled and actively controlling the cycle _LOGGER.info("Running PWM cycle with state: %s", pwm_state) - if pwm_state == PWMState.ON and self.max_error > -DEADBAND: + if pwm_state == PWMState.ON: self._setpoint = self.minimum_setpoint _LOGGER.debug("Setting setpoint to minimum: %.1f°C", self.minimum_setpoint) else: From d737ccf00d479173ff93cec2f24be9892919bfab Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 31 Dec 2024 16:42:47 +0100 Subject: [PATCH 209/213] Cleanup --- custom_components/sat/mqtt/opentherm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/mqtt/opentherm.py b/custom_components/sat/mqtt/opentherm.py index 4ac91cb4..8185eaec 100644 --- a/custom_components/sat/mqtt/opentherm.py +++ b/custom_components/sat/mqtt/opentherm.py @@ -1,4 +1,4 @@ -from __future__ import annotations, annotations +from __future__ import annotations import logging From 90b053e9740f9d3555a6412a1df13550cbbe0abf Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 31 Dec 2024 17:16:20 +0100 Subject: [PATCH 210/213] Delay the Synchro Sensor with stating a problem --- custom_components/sat/binary_sensor.py | 52 ++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/custom_components/sat/binary_sensor.py b/custom_components/sat/binary_sensor.py index 2321653f..ccfd07d1 100644 --- a/custom_components/sat/binary_sensor.py +++ b/custom_components/sat/binary_sensor.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorDeviceClass @@ -41,7 +42,38 @@ async def async_setup_entry(_hass: HomeAssistant, _config_entry: ConfigEntry, _a _async_add_entities([SatCentralHeatingSynchroSensor(coordinator, _config_entry, climate)]) -class SatControlSetpointSynchroSensor(SatClimateEntity, BinarySensorEntity): +class SatSynchroSensor: + """Mixin to add delayed state change for binary sensors.""" + + def __init__(self, delay: int = 5): + """Initialize the mixin with a delay.""" + self._delay = delay + self._last_mismatch = None + + def state_delayed(self, condition: bool) -> bool: + """Determine the delayed state based on a condition.""" + if not condition: + self._last_mismatch = None + return False + + if self._last_mismatch is None: + self._last_mismatch = self._get_current_time() + + if self._get_current_time() - self._last_mismatch >= self._delay: + return True + + return False + + @staticmethod + def _get_current_time() -> float: + """Get the current time in seconds since epoch.""" + return asyncio.get_event_loop().time() + + +class SatControlSetpointSynchroSensor(SatSynchroSensor, SatClimateEntity, BinarySensorEntity): + def __init__(self, coordinator, _config_entry, climate): + SatSynchroSensor.__init__(self) + SatClimateEntity.__init__(self, coordinator, _config_entry, climate) @property def name(self): @@ -61,7 +93,7 @@ def available(self): @property def is_on(self): """Return the state of the sensor.""" - return round(self._climate.setpoint, 1) != round(self._coordinator.setpoint, 1) + return self.state_delayed(round(self._climate.setpoint, 1) != round(self._coordinator.setpoint, 1)) @property def unique_id(self): @@ -69,7 +101,10 @@ def unique_id(self): return f"{self._config_entry.data.get(CONF_NAME).lower()}-control-setpoint-synchro" -class SatRelativeModulationSynchroSensor(SatClimateEntity, BinarySensorEntity): +class SatRelativeModulationSynchroSensor(SatSynchroSensor, SatClimateEntity, BinarySensorEntity): + def __init__(self, coordinator, _config_entry, climate): + SatSynchroSensor.__init__(self) + SatClimateEntity.__init__(self, coordinator, _config_entry, climate) @property def name(self): @@ -89,7 +124,7 @@ def available(self): @property def is_on(self): """Return the state of the sensor.""" - return int(self._climate.relative_modulation_value) != int(self._coordinator.maximum_relative_modulation_value) + return self.state_delayed(int(self._climate.relative_modulation_value) != int(self._coordinator.maximum_relative_modulation_value)) @property def unique_id(self): @@ -97,7 +132,10 @@ def unique_id(self): return f"{self._config_entry.data.get(CONF_NAME).lower()}-relative-modulation-synchro" -class SatCentralHeatingSynchroSensor(SatClimateEntity, BinarySensorEntity): +class SatCentralHeatingSynchroSensor(SatSynchroSensor, SatClimateEntity, BinarySensorEntity): + def __init__(self, coordinator, _config_entry, climate): + SatSynchroSensor.__init__(self) + SatClimateEntity.__init__(self, coordinator, _config_entry, climate) @property def name(self) -> str: @@ -120,11 +158,11 @@ def is_on(self) -> bool: device_active = self._coordinator.device_active climate_hvac_action = self._climate.state_attributes.get("hvac_action") - return not ( + return self.state_delayed(not ( (climate_hvac_action == HVACAction.OFF and not device_active) or (climate_hvac_action == HVACAction.IDLE and not device_active) or (climate_hvac_action == HVACAction.HEATING and device_active) - ) + )) @property def unique_id(self) -> str: From 9f83c72a048a9e3f9fbc86f6b3bcd283f8db9e77 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 31 Dec 2024 18:01:37 +0100 Subject: [PATCH 211/213] Zip the custom_components folder when releasing a version --- .github/workflows/release.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ee933310 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,17 @@ +name: Release + +on: + release: + types: [ published ] + +jobs: + release: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: vimtor/action-zip@v1.2 + with: + files: custom_components/ + dest: sat.zip \ No newline at end of file From cf69a410a3a165d4f7f4c923b51e7616544b6dc4 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 31 Dec 2024 18:09:51 +0100 Subject: [PATCH 212/213] Increase the delay --- custom_components/sat/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sat/binary_sensor.py b/custom_components/sat/binary_sensor.py index ccfd07d1..4f938c87 100644 --- a/custom_components/sat/binary_sensor.py +++ b/custom_components/sat/binary_sensor.py @@ -45,7 +45,7 @@ async def async_setup_entry(_hass: HomeAssistant, _config_entry: ConfigEntry, _a class SatSynchroSensor: """Mixin to add delayed state change for binary sensors.""" - def __init__(self, delay: int = 5): + def __init__(self, delay: int = 30): """Initialize the mixin with a delay.""" self._delay = delay self._last_mismatch = None From 4d6bca4d4ac474c13cf8db22e527935e2df59717 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Tue, 31 Dec 2024 18:50:28 +0100 Subject: [PATCH 213/213] Improvements on PWM cycle reset --- custom_components/sat/pwm.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/custom_components/sat/pwm.py b/custom_components/sat/pwm.py index 2494806e..161f7d51 100644 --- a/custom_components/sat/pwm.py +++ b/custom_components/sat/pwm.py @@ -75,10 +75,10 @@ async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: self._last_boiler_temperature = boiler.temperature _LOGGER.debug("Initialized last boiler temperature to %.1f°C", boiler.temperature) - if self._first_duty_cycle_start and (monotonic() - self._first_duty_cycle_start) > 3600: + if self._first_duty_cycle_start is None or (monotonic() - self._first_duty_cycle_start) > 3600: self._cycles = 0 - self._first_duty_cycle_start = None - _LOGGER.info("CYCLES count reset after an hour.") + self._first_duty_cycle_start = monotonic() + _LOGGER.info("CYCLES count reset for the rolling hour.") elapsed = monotonic() - self._last_update self._duty_cycle = self._calculate_duty_cycle(requested_setpoint, boiler) @@ -98,9 +98,6 @@ async def update(self, requested_setpoint: float, boiler: BoilerState) -> None: # State transitions for PWM if self._state != PWMState.ON and self._duty_cycle[0] >= HEATER_STARTUP_TIMEFRAME and ( elapsed >= self._duty_cycle[1] or self._state == PWMState.IDLE): - if self._first_duty_cycle_start is None: - self._first_duty_cycle_start = monotonic() - if self._cycles >= self._max_cycles: _LOGGER.info("Reached max cycles per hour, preventing new duty cycle.") return