From a932878a17f6e8a8ca1bca78def8eb9e0b765d59 Mon Sep 17 00:00:00 2001 From: xX7 <5272979+xX7@users.noreply.github.com> Date: Mon, 4 Dec 2023 19:28:17 +0000 Subject: [PATCH 1/3] Moved const.py bridge.py and model_quirks.py Using CalibrationType/CalibrationMode instead of variables Renamed FIX_CALIBRATION to AGGRESIVE_CALIBRATION Renamed bridge.py to delegate.py Removed unused "calibration_round" Local calibration calculations now respect the entity's step size Calibration calculations generally respect the user-defined tolerance "No Calibration" is now disabling target-temp/local calibration More calibration debug logs --- .../better_thermostat/__init__.py | 14 +- .../{utils/bridge.py => adapters/delegate.py} | 0 .../better_thermostat/calibration.py | 242 ++++++++++++++++++ .../better_thermostat/climate.py | 10 +- .../better_thermostat/config_flow.py | 6 +- .../better_thermostat/diagnostics.py | 4 +- .../better_thermostat/events/temperature.py | 2 +- .../better_thermostat/events/trv.py | 65 ++--- .../{utils => model_fixes}/model_quirks.py | 0 .../better_thermostat/{ => utils}/const.py | 5 +- .../better_thermostat/utils/controlling.py | 59 +++-- .../better_thermostat/utils/helpers.py | 241 +++-------------- 12 files changed, 350 insertions(+), 298 deletions(-) rename custom_components/better_thermostat/{utils/bridge.py => adapters/delegate.py} (100%) create mode 100644 custom_components/better_thermostat/calibration.py rename custom_components/better_thermostat/{utils => model_fixes}/model_quirks.py (100%) rename custom_components/better_thermostat/{ => utils}/const.py (94%) diff --git a/custom_components/better_thermostat/__init__.py b/custom_components/better_thermostat/__init__.py index 40bb258a..cdfabb99 100644 --- a/custom_components/better_thermostat/__init__.py +++ b/custom_components/better_thermostat/__init__.py @@ -5,13 +5,13 @@ from homeassistant.config_entries import ConfigEntry import voluptuous as vol -from .const import ( - CONF_FIX_CALIBRATION, +from .utils.const import ( CONF_CALIBRATION_MODE, CONF_HEATER, CONF_NO_SYSTEM_MODE_OFF, CONF_WINDOW_TIMEOUT, CONF_WINDOW_TIMEOUT_AFTER, + CalibrationMode ) _LOGGER = logging.getLogger(__name__) @@ -57,7 +57,7 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry): if config_entry.version == 1: new = {**config_entry.data} for trv in new[CONF_HEATER]: - trv["advanced"].update({CONF_FIX_CALIBRATION: False}) + trv["advanced"].update({CalibrationMode.AGGRESIVE_CALIBRATION: False}) config_entry.version = 2 hass.config_entries.async_update_entry(config_entry, data=new) @@ -71,12 +71,12 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry): new = {**config_entry.data} for trv in new[CONF_HEATER]: if ( - CONF_FIX_CALIBRATION in trv["advanced"] - and trv["advanced"][CONF_FIX_CALIBRATION] + CalibrationMode.AGGRESIVE_CALIBRATION in trv["advanced"] + and trv["advanced"][CalibrationMode.AGGRESIVE_CALIBRATION] ): - trv["advanced"].update({CONF_CALIBRATION_MODE: CONF_FIX_CALIBRATION}) + trv["advanced"].update({CONF_CALIBRATION_MODE: CalibrationMode.AGGRESIVE_CALIBRATION}) else: - trv["advanced"].update({CONF_CALIBRATION_MODE: "default"}) + trv["advanced"].update({CONF_CALIBRATION_MODE: CalibrationMode.DEFAULT}) config_entry.version = 4 hass.config_entries.async_update_entry(config_entry, data=new) diff --git a/custom_components/better_thermostat/utils/bridge.py b/custom_components/better_thermostat/adapters/delegate.py similarity index 100% rename from custom_components/better_thermostat/utils/bridge.py rename to custom_components/better_thermostat/adapters/delegate.py diff --git a/custom_components/better_thermostat/calibration.py b/custom_components/better_thermostat/calibration.py new file mode 100644 index 00000000..5cb232a0 --- /dev/null +++ b/custom_components/better_thermostat/calibration.py @@ -0,0 +1,242 @@ +"""Helper functions for the Better Thermostat component.""" +import logging +from typing import Union + +from homeassistant.components.climate.const import HVACAction + +from custom_components.better_thermostat.utils.const import ( + CalibrationMode, + CONF_PROTECT_OVERHEATING, +) + +from custom_components.better_thermostat.utils.helpers import ( + convert_to_float, + round_down_to_half_degree, + round_by_steps, + heating_power_valve_position +) + +from custom_components.better_thermostat.model_fixes.model_quirks import ( + fix_local_calibration, + fix_target_temperature_calibration, +) + +_LOGGER = logging.getLogger(__name__) + + +def calculate_calibration_local(self, entity_id) -> Union[float, None]: + """Calculate local delta to adjust the setpoint of the TRV based on the air temperature of the external sensor. + + This calibration is for devices with local calibration option, it syncs the current temperature of the TRV to the target temperature of + the external sensor. + + Parameters + ---------- + self : + self instance of better_thermostat + + Returns + ------- + float + new local calibration delta + """ + _context = "_calculate_calibration_local()" + + if None in (self.cur_temp, self.bt_target_temp, self.old_internal_temp): + return None + + _old_trv_temp_s = self.old_internal_temp + _cur_trv_temp_s = self.real_trvs[entity_id]["current_temperature"] + + _calibration_steps = self.real_trvs[entity_id]["local_calibration_steps"] + + _old_external_temp = self.old_external_temp + _cur_external_temp = self.cur_temp + + _cur_target_temp = self.bt_target_temp + + _cur_trv_temp_f = convert_to_float( + str(_cur_trv_temp_s), self.name, _context + ) + + # check if we need to calculate + if ( + _cur_trv_temp_s == _old_trv_temp_s + and _cur_external_temp == _old_external_temp + ): + return None + + # Setting old variables + self.old_internal_temp = _cur_trv_temp_s + self.old_external_temp = _cur_external_temp + + _current_trv_calibration = convert_to_float( + str(self.real_trvs[entity_id]["last_calibration"]), self.name, _context + ) + + if None in (_current_trv_calibration, _cur_external_temp, _cur_trv_temp_f, _calibration_steps): + _LOGGER.warning( + f"better thermostat {self.name}: {entity_id} Could not calculate local calibration in {_context}:" + f" trv_calibration: {_current_trv_calibration}, trv_temp: {_cur_trv_temp_f}, external_temp: {_cur_external_temp}" + f" calibration_steps: {_calibration_steps}" + ) + return None + + _new_trv_calibration = (_cur_external_temp - _cur_trv_temp_f) + _current_trv_calibration + + _calibration_mode = self.real_trvs[entity_id]["advanced"].get( + "calibration_mode", CalibrationMode.DEFAULT + ) + + if _calibration_mode == CalibrationMode.AGGRESIVE_CALIBRATION: + if self.attr_hvac_action == HVACAction.HEATING: + if _new_trv_calibration > -2.5: + _new_trv_calibration -= 2.5 + + if _calibration_mode == CalibrationMode.HEATING_POWER_CALIBRATION: + if self.attr_hvac_action == HVACAction.HEATING: + _valve_position = heating_power_valve_position(self, entity_id) + _new_trv_calibration = _current_trv_calibration - ( + (self.real_trvs[entity_id]["local_calibration_min"] + _cur_trv_temp_f) + * _valve_position + ) + + # Respecting tolerance in all calibration modes, delaying heat + if self.attr_hvac_action == HVACAction.IDLE: + if _new_trv_calibration < 0.0: + _new_trv_calibration += self.tolerance + + _new_trv_calibration = fix_local_calibration( + self, entity_id, _new_trv_calibration + ) + + _overheating_protection = self.real_trvs[entity_id]["advanced"].get( + CONF_PROTECT_OVERHEATING, False + ) + + if _overheating_protection is True: + if _cur_external_temp >= _cur_target_temp: + _new_trv_calibration += (_cur_external_temp - _cur_target_temp) * 10.0 + + # Adjust based on the steps allowed by the local calibration entity + _new_trv_calibration = round_by_steps( + _new_trv_calibration, + _calibration_steps + ) + + # Compare against min/max + if _new_trv_calibration > float( + self.real_trvs[entity_id]["local_calibration_max"] + ): + _new_trv_calibration = float( + self.real_trvs[entity_id]["local_calibration_max"] + ) + elif _new_trv_calibration < float( + self.real_trvs[entity_id]["local_calibration_min"] + ): + _new_trv_calibration = float( + self.real_trvs[entity_id]["local_calibration_min"] + ) + + _new_trv_calibration = convert_to_float( + str(_new_trv_calibration), self.name, _context + ) + + _logmsg = "better_thermostat %s: %s - new local calibration: %s | external_temp: %s, "\ + "trv_temp: %s, calibration: %s" + + _LOGGER.debug( + _logmsg, + self.name, + entity_id, + _new_trv_calibration, + _cur_external_temp, + _cur_trv_temp_f, + _current_trv_calibration + ) + + return _new_trv_calibration + +def calculate_calibration_setpoint(self, entity_id) -> Union[float, None]: + """Calculate new setpoint for the TRV based on its own temperature measurement and the air temperature of the external sensor. + + This calibration is for devices with no local calibration option, it syncs the target temperature of the TRV to a new target + temperature based on the current temperature of the external sensor. + + Parameters + ---------- + self : + self instance of better_thermostat + + Returns + ------- + float + new target temp with calibration + """ + if None in (self.cur_temp, self.bt_target_temp): + return None + + _cur_trv_temp_s = self.real_trvs[entity_id]["current_temperature"] + + _cur_external_temp = self.cur_temp + _cur_target_temp = self.bt_target_temp + + if None in (_cur_target_temp, _cur_external_temp, _cur_trv_temp_s): + return None + + _calibrated_setpoint = (_cur_target_temp - _cur_external_temp) + _cur_trv_temp_s + + _calibration_mode = self.real_trvs[entity_id]["advanced"].get( + "calibration_mode", CalibrationMode.DEFAULT + ) + + if _calibration_mode == CalibrationMode.AGGRESIVE_CALIBRATION: + if self.attr_hvac_action == HVACAction.HEATING: + if _calibrated_setpoint - _cur_trv_temp_s < 2.5: + _calibrated_setpoint += 2.5 + + if _calibration_mode == CalibrationMode.HEATING_POWER_CALIBRATION: + if self.attr_hvac_action == HVACAction.HEATING: + valve_position = heating_power_valve_position(self, entity_id) + _calibrated_setpoint = _cur_trv_temp_s + ( + (self.real_trvs[entity_id]["max_temp"] - _cur_trv_temp_s) * valve_position + ) + + if self.attr_hvac_action == HVACAction.IDLE: + if _calibrated_setpoint - _cur_trv_temp_s > 0.0: + _calibrated_setpoint -= self.tolerance + + _calibrated_setpoint = fix_target_temperature_calibration( + self, entity_id, _calibrated_setpoint + ) + + _overheating_protection = self.real_trvs[entity_id]["advanced"].get( + CONF_PROTECT_OVERHEATING, False + ) + + if _overheating_protection is True: + if _cur_external_temp >= _cur_target_temp: + _calibrated_setpoint -= (_cur_external_temp - _cur_target_temp) * 10.0 + + _calibrated_setpoint = round_down_to_half_degree(_calibrated_setpoint) + + # check if new setpoint is inside the TRV's range, else set to min or max + if _calibrated_setpoint < self.real_trvs[entity_id]["min_temp"]: + _calibrated_setpoint = self.real_trvs[entity_id]["min_temp"] + if _calibrated_setpoint > self.real_trvs[entity_id]["max_temp"]: + _calibrated_setpoint = self.real_trvs[entity_id]["max_temp"] + + _logmsg = "better_thermostat %s: %s - new setpoint calibration: %s | external_temp: %s, "\ + "target_temp: %s, trv_temp: %s" + + _LOGGER.debug( + _logmsg, + self.name, + entity_id, + _calibrated_setpoint, + _cur_external_temp, + _cur_target_temp, + _cur_trv_temp_s + ) + + return _calibrated_setpoint diff --git a/custom_components/better_thermostat/climate.py b/custom_components/better_thermostat/climate.py index 73268ef7..4e5c4018 100644 --- a/custom_components/better_thermostat/climate.py +++ b/custom_components/better_thermostat/climate.py @@ -12,15 +12,16 @@ from .utils.watcher import check_all_entities from .utils.weather import check_ambient_air_temperature, check_weather -from .utils.bridge import ( +from .adapters.delegate import ( get_current_offset, + get_offset_steps, get_min_offset, get_max_offset, init, load_adapter, ) -from .utils.model_quirks import load_model_quirks +from .model_fixes.model_quirks import load_model_quirks from .utils.helpers import convert_to_float, find_battery_entity, get_hvac_bt_mode from homeassistant.helpers import entity_platform @@ -56,7 +57,7 @@ from homeassistant.components.group.util import reduce_attribute -from .const import ( +from .utils.const import ( ATTR_STATE_BATTERIES, ATTR_STATE_CALL_FOR_HEAT, ATTR_STATE_ERRORS, @@ -780,6 +781,9 @@ async def startup(self): self.real_trvs[trv]["local_calibration_max"] = await get_max_offset( self, trv ) + self.real_trvs[trv]["local_calibration_steps"] = await get_offset_steps( + self, trv + ) else: self.real_trvs[trv]["last_calibration"] = 0 diff --git a/custom_components/better_thermostat/config_flow.py b/custom_components/better_thermostat/config_flow.py index 47c55855..1ed13492 100644 --- a/custom_components/better_thermostat/config_flow.py +++ b/custom_components/better_thermostat/config_flow.py @@ -10,11 +10,11 @@ from homeassistant.helpers import config_validation as cv -from .utils.bridge import load_adapter +from .adapters.delegate import load_adapter from .utils.helpers import get_device_model, get_trv_intigration -from .const import ( +from .utils.const import ( CONF_COOLER, CONF_PROTECT_OVERHEATING, CONF_CALIBRATION, @@ -75,7 +75,7 @@ options=[ selector.SelectOptionDict(value=CalibrationMode.DEFAULT, label="Normal"), selector.SelectOptionDict( - value=CalibrationMode.FIX_CALIBRATION, label="Agressive" + value=CalibrationMode.AGGRESIVE_CALIBRATION, label="Agressive" ), selector.SelectOptionDict( value=CalibrationMode.HEATING_POWER_CALIBRATION, label="AI Time Based" diff --git a/custom_components/better_thermostat/diagnostics.py b/custom_components/better_thermostat/diagnostics.py index f13b74dc..be7005bf 100644 --- a/custom_components/better_thermostat/diagnostics.py +++ b/custom_components/better_thermostat/diagnostics.py @@ -4,9 +4,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .utils.bridge import load_adapter +from .adapters.delegate import load_adapter -from .const import CONF_HEATER, CONF_SENSOR, CONF_SENSOR_WINDOW +from .utils.const import CONF_HEATER, CONF_SENSOR, CONF_SENSOR_WINDOW async def async_get_config_entry_diagnostics( diff --git a/custom_components/better_thermostat/events/temperature.py b/custom_components/better_thermostat/events/temperature.py index 762846be..cd12284c 100644 --- a/custom_components/better_thermostat/events/temperature.py +++ b/custom_components/better_thermostat/events/temperature.py @@ -1,6 +1,6 @@ import logging -from custom_components.better_thermostat.const import CONF_HOMATICIP +from custom_components.better_thermostat.utils.const import CONF_HOMATICIP from ..utils.helpers import convert_to_float from datetime import datetime diff --git a/custom_components/better_thermostat/events/trv.py b/custom_components/better_thermostat/events/trv.py index 60ef0c2d..c6f0bbc5 100644 --- a/custom_components/better_thermostat/events/trv.py +++ b/custom_components/better_thermostat/events/trv.py @@ -2,7 +2,7 @@ from datetime import datetime import logging from typing import Union -from custom_components.better_thermostat.const import CONF_HOMATICIP +from custom_components.better_thermostat.utils.const import CONF_HOMATICIP from homeassistant.components.climate.const import ( HVACMode, @@ -11,14 +11,18 @@ ) from homeassistant.core import State, callback from homeassistant.components.group.util import find_state_attributes -from ..utils.helpers import ( - calculate_local_setpoint_delta, - calculate_setpoint_override, +from custom_components.better_thermostat.utils.helpers import ( convert_to_float, - mode_remap, - round_to_half_degree, + mode_remap +) +from custom_components.better_thermostat.adapters.delegate import get_current_offset + +from custom_components.better_thermostat.utils.const import CalibrationType + +from custom_components.better_thermostat.calibration import ( + calculate_calibration_local, + calculate_calibration_setpoint ) -from custom_components.better_thermostat.utils.bridge import get_current_offset _LOGGER = logging.getLogger(__name__) @@ -314,7 +318,7 @@ def convert_outbound_states(self, entity_id, hvac_mode) -> Union[dict, None]: _new_heating_setpoint = None try: - _calibration_type = self.real_trvs[entity_id].get("calibration", 1) + _calibration_type = self.real_trvs[entity_id]["advanced"].get("calibration") if _calibration_type is None: _LOGGER.warning( @@ -322,52 +326,21 @@ def convert_outbound_states(self, entity_id, hvac_mode) -> Union[dict, None]: self.name, ) _new_heating_setpoint = self.bt_target_temp - _new_local_calibration = round_to_half_degree( - calculate_local_setpoint_delta(self, entity_id) - ) + _new_local_calibration = calculate_calibration_local(self, entity_id) + if _new_local_calibration is None: return None else: - if _calibration_type == 0: - _round_calibration = self.real_trvs[entity_id]["advanced"].get( - "calibration_round" + if _calibration_type == CalibrationType.LOCAL_BASED: + _new_local_calibration = calculate_calibration_local( + self, entity_id ) - if _round_calibration is not None and ( - ( - isinstance(_round_calibration, str) - and _round_calibration.lower() == "true" - ) - or _round_calibration is True - ): - _new_local_calibration = round_to_half_degree( - calculate_local_setpoint_delta(self, entity_id) - ) - else: - _new_local_calibration = calculate_local_setpoint_delta( - self, entity_id - ) - _new_heating_setpoint = self.bt_target_temp - elif _calibration_type == 1: - _round_calibration = self.real_trvs[entity_id]["advanced"].get( - "calibration_round" - ) - - if _round_calibration is not None and ( - ( - isinstance(_round_calibration, str) - and _round_calibration.lower() == "true" - ) - or _round_calibration is True - ): - _new_heating_setpoint = round_to_half_degree( - calculate_setpoint_override(self, entity_id) - ) - else: - _new_heating_setpoint = calculate_setpoint_override(self, entity_id) + elif _calibration_type == CalibrationType.TARGET_TEMP_BASED: + _new_heating_setpoint = calculate_calibration_setpoint(self, entity_id) _system_modes = self.real_trvs[entity_id]["hvac_modes"] _has_system_mode = False diff --git a/custom_components/better_thermostat/utils/model_quirks.py b/custom_components/better_thermostat/model_fixes/model_quirks.py similarity index 100% rename from custom_components/better_thermostat/utils/model_quirks.py rename to custom_components/better_thermostat/model_fixes/model_quirks.py diff --git a/custom_components/better_thermostat/const.py b/custom_components/better_thermostat/utils/const.py similarity index 94% rename from custom_components/better_thermostat/const.py rename to custom_components/better_thermostat/utils/const.py index 48f63ea2..58d61e6d 100644 --- a/custom_components/better_thermostat/const.py +++ b/custom_components/better_thermostat/utils/const.py @@ -44,9 +44,6 @@ CONF_CHILD_LOCK = "child_lock" CONF_PROTECT_OVERHEATING = "protect_overheating" CONF_CALIBRATION_MODE = "calibration_mode" -CONF_FIX_CALIBRATION = "fix_calibration" -CONF_HEATING_POWER_CALIBRATION = "heating_power_calibration" -CONF_NO_CALIBRATION = "no_calibration" CONF_HEAT_AUTO_SWAPPED = "heat_auto_swapped" CONF_MODEL = "model" CONF_HOMATICIP = "homaticip" @@ -97,6 +94,6 @@ class CalibrationMode(StrEnum): """Calibration mode.""" DEFAULT = "default" - FIX_CALIBRATION = "fix_calibration" + AGGRESIVE_CALIBRATION = "fix_calibration" HEATING_POWER_CALIBRATION = "heating_power_calibration" NO_CALIBRATION = "no_calibration" diff --git a/custom_components/better_thermostat/utils/controlling.py b/custom_components/better_thermostat/utils/controlling.py index 01894906..a95100c3 100644 --- a/custom_components/better_thermostat/utils/controlling.py +++ b/custom_components/better_thermostat/utils/controlling.py @@ -1,22 +1,31 @@ import asyncio import logging -from custom_components.better_thermostat.utils.model_quirks import ( +from homeassistant.components.climate.const import HVACMode + +from custom_components.better_thermostat.model_fixes.model_quirks import ( override_set_hvac_mode, override_set_temperature, ) -from .bridge import ( +from custom_components.better_thermostat.adapters.delegate import ( set_offset, get_current_offset, - get_offset_steps, set_temperature, - set_hvac_mode, + set_hvac_mode ) -from ..events.trv import convert_outbound_states, update_hvac_action -from homeassistant.components.climate.const import HVACMode -from .helpers import convert_to_float, calibration_round +from custom_components.better_thermostat.events.trv import ( + convert_outbound_states, + update_hvac_action +) + +from custom_components.better_thermostat.utils.helpers import ( + convert_to_float +) + +from custom_components.better_thermostat.utils.const import CalibrationMode + _LOGGER = logging.getLogger(__name__) @@ -103,6 +112,9 @@ async def control_trv(self, heater_entity_id=None): _temperature = _remapped_states.get("temperature", None) _calibration = _remapped_states.get("local_temperature_calibration", None) + _calibration_mode = self.real_trvs[heater_entity_id]["advanced"].get( + "calibration_mode", CalibrationMode.DEFAULT + ) _new_hvac_mode = handle_window_open(self, _remapped_states) @@ -213,10 +225,13 @@ async def control_trv(self, heater_entity_id=None): asyncio.create_task(check_system_mode(self, heater_entity_id)) # set new calibration offset - if _calibration is not None and _new_hvac_mode != HVACMode.OFF: - old_calibration = await get_current_offset(self, heater_entity_id) - step_calibration = await get_offset_steps(self, heater_entity_id) - if old_calibration is None or step_calibration is None: + if (_calibration is not None + and _new_hvac_mode != HVACMode.OFF + and _calibration_mode != CalibrationMode.NO_CALIBRATION + ): + _current_calibration_s = await get_current_offset(self, heater_entity_id) + + if _current_calibration_s is None: _LOGGER.error( "better_thermostat %s: calibration fatal error %s", self.name, @@ -226,25 +241,22 @@ async def control_trv(self, heater_entity_id=None): self.ignore_states = False self.real_trvs[heater_entity_id]["ignore_trv_states"] = False return True - current_calibration = convert_to_float( - str(old_calibration), self.name, "controlling()" + + _current_calibration = convert_to_float( + str(_current_calibration_s), self.name, "controlling()" ) - if step_calibration.is_integer(): - _calibration = calibration_round( - float(str(format(float(_calibration), ".1f"))) - ) - else: - _calibration = float(str(format(float(_calibration), ".1f"))) - old = self.real_trvs[heater_entity_id].get( - "last_calibration", current_calibration + _calibration = float(str(_calibration)) + + _old_calibration = self.real_trvs[heater_entity_id].get( + "last_calibration", _current_calibration ) if self.real_trvs[heater_entity_id][ "calibration_received" - ] is True and float(old) != float(_calibration): + ] is True and float(_old_calibration) != float(_calibration): _LOGGER.debug( - f"better_thermostat {self.name}: TO TRV set_local_temperature_calibration: {heater_entity_id} from: {old} to: {_calibration}" + f"better_thermostat {self.name}: TO TRV set_local_temperature_calibration: {heater_entity_id} from: {_old_calibration} to: {_calibration}" ) await set_offset(self, heater_entity_id, _calibration) self.real_trvs[heater_entity_id]["calibration_received"] = False @@ -252,6 +264,7 @@ async def control_trv(self, heater_entity_id=None): # set new target temperature if ( _temperature is not None + and _calibration_mode != CalibrationMode.NO_CALIBRATION and _new_hvac_mode != HVACMode.OFF or _no_off_system_mode ): diff --git a/custom_components/better_thermostat/utils/helpers.py b/custom_components/better_thermostat/utils/helpers.py index 6e44b86e..a54005f5 100644 --- a/custom_components/better_thermostat/utils/helpers.py +++ b/custom_components/better_thermostat/utils/helpers.py @@ -8,19 +8,16 @@ from homeassistant.components.climate.const import HVACMode, HVACAction -from custom_components.better_thermostat.utils.model_quirks import ( - fix_local_calibration, - fix_target_temperature_calibration, -) - - -from ..const import ( +from custom_components.better_thermostat.utils.const import ( CONF_HEAT_AUTO_SWAPPED, - CONF_HEATING_POWER_CALIBRATION, - CONF_FIX_CALIBRATION, CONF_PROTECT_OVERHEATING, + CalibrationMode ) +from custom_components.better_thermostat.model_fixes.model_quirks import ( + fix_local_calibration, + fix_target_temperature_calibration, +) _LOGGER = logging.getLogger(__name__) @@ -72,206 +69,6 @@ def mode_remap(self, entity_id, hvac_mode: str, inbound: bool = False) -> str: return HVACMode.OFF -def calculate_local_setpoint_delta(self, entity_id) -> Union[float, None]: - """Calculate local delta to adjust the setpoint of the TRV based on the air temperature of the external sensor. - - This calibration is for devices with local calibration option, it syncs the current temperature of the TRV to the target temperature of - the external sensor. - - Parameters - ---------- - self : - self instance of better_thermostat - - Returns - ------- - float - new local calibration delta - """ - _context = "calculate_local_setpoint_delta()" - - if None in (self.cur_temp, self.bt_target_temp, self.old_internal_temp): - return None - - # check if we need to calculate - if ( - self.real_trvs[entity_id]["current_temperature"] == self.old_internal_temp - and self.cur_temp == self.old_external_temp - ): - return None - - _cur_trv_temp = convert_to_float( - str(self.real_trvs[entity_id]["current_temperature"]), self.name, _context - ) - - _calibration_delta = float( - str(format(float(abs(_cur_trv_temp - self.cur_temp)), ".1f")) - ) - - if _calibration_delta <= 0.5: - return None - - self.old_internal_temp = self.real_trvs[entity_id]["current_temperature"] - self.old_external_temp = self.cur_temp - - _current_trv_calibration = round_to_half_degree( - convert_to_float( - str(self.real_trvs[entity_id]["last_calibration"]), self.name, _context - ) - ) - - if None in (_current_trv_calibration, self.cur_temp, _cur_trv_temp): - _LOGGER.warning( - f"better thermostat {self.name}: {entity_id} Could not calculate local setpoint delta in {_context}:" - f" current_trv_calibration: {_current_trv_calibration}, current_trv_temp: {_cur_trv_temp}, cur_temp: {self.cur_temp}" - ) - return None - - _new_local_calibration = (self.cur_temp - _cur_trv_temp) + _current_trv_calibration - - _calibration_mode = self.real_trvs[entity_id]["advanced"].get( - "calibration_mode", "default" - ) - if _calibration_mode == CONF_FIX_CALIBRATION: - # _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp)) - # if _temp_diff > 0.30 and _new_local_calibration > -2.5: - # _new_local_calibration -= 2.5 - if self.attr_hvac_action == HVACAction.HEATING: - if _new_local_calibration > -2.5: - _new_local_calibration -= 2.5 - - elif _calibration_mode == CONF_HEATING_POWER_CALIBRATION: - # _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp)) - # if _temp_diff > 0.0: - # valve_position = heating_power_valve_position(self, entity_id) - # _new_local_calibration = _current_trv_calibration - ( - # (self.real_trvs[entity_id]["local_calibration_min"] + _cur_trv_temp) - # * valve_position - # ) - if self.attr_hvac_action == HVACAction.HEATING: - # if _temp_diff > 0.0: - valve_position = heating_power_valve_position(self, entity_id) - _new_local_calibration = _current_trv_calibration - ( - (self.real_trvs[entity_id]["local_calibration_min"] + _cur_trv_temp) - * valve_position - ) - elif self.attr_hvac_action == HVACAction.IDLE: - if _new_local_calibration < 0.0: - _new_local_calibration += self.tolerance - - _new_local_calibration = fix_local_calibration( - self, entity_id, _new_local_calibration - ) - - _overheating_protection = self.real_trvs[entity_id]["advanced"].get( - CONF_PROTECT_OVERHEATING, False - ) - - if _overheating_protection is True: - if self.cur_temp >= self.bt_target_temp: - _new_local_calibration += (self.cur_temp - self.bt_target_temp) * 10.0 - - _new_local_calibration = round_down_to_half_degree(_new_local_calibration) - - if _new_local_calibration > float( - self.real_trvs[entity_id]["local_calibration_max"] - ): - _new_local_calibration = float( - self.real_trvs[entity_id]["local_calibration_max"] - ) - elif _new_local_calibration < float( - self.real_trvs[entity_id]["local_calibration_min"] - ): - _new_local_calibration = float( - self.real_trvs[entity_id]["local_calibration_min"] - ) - - _new_local_calibration = convert_to_float( - str(_new_local_calibration), self.name, _context - ) - - _LOGGER.debug( - "better_thermostat %s: %s - output calib: %s", - self.name, - entity_id, - _new_local_calibration, - ) - - return convert_to_float(str(_new_local_calibration), self.name, _context) - - -def calculate_setpoint_override(self, entity_id) -> Union[float, None]: - """Calculate new setpoint for the TRV based on its own temperature measurement and the air temperature of the external sensor. - - This calibration is for devices with no local calibration option, it syncs the target temperature of the TRV to a new target - temperature based on the current temperature of the external sensor. - - Parameters - ---------- - self : - self instance of better_thermostat - - Returns - ------- - float - new target temp with calibration - """ - if None in (self.cur_temp, self.bt_target_temp): - return None - - _cur_trv_temp = self.hass.states.get(entity_id).attributes["current_temperature"] - if None in (self.bt_target_temp, self.cur_temp, _cur_trv_temp): - return None - - _calibrated_setpoint = (self.bt_target_temp - self.cur_temp) + _cur_trv_temp - - _calibration_mode = self.real_trvs[entity_id]["advanced"].get( - "calibration_mode", "default" - ) - if _calibration_mode == CONF_FIX_CALIBRATION: - # _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp)) - if self.attr_hvac_action == HVACAction.HEATING: - if _calibrated_setpoint - _cur_trv_temp < 2.5: - _calibrated_setpoint += 2.5 - elif self.attr_hvac_action == HVACAction.IDLE: - if _calibrated_setpoint - _cur_trv_temp > 0.0: - _calibrated_setpoint -= self.tolerance - - elif _calibration_mode == CONF_HEATING_POWER_CALIBRATION: - # _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp)) - if self.attr_hvac_action == HVACAction.HEATING: - # if _temp_diff > 0.0: - valve_position = heating_power_valve_position(self, entity_id) - _calibrated_setpoint = _cur_trv_temp + ( - (self.real_trvs[entity_id]["max_temp"] - _cur_trv_temp) * valve_position - ) - elif self.attr_hvac_action == HVACAction.IDLE: - if _calibrated_setpoint - _cur_trv_temp > 0.0: - _calibrated_setpoint -= self.tolerance - - _calibrated_setpoint = fix_target_temperature_calibration( - self, entity_id, _calibrated_setpoint - ) - - _overheating_protection = self.real_trvs[entity_id]["advanced"].get( - CONF_PROTECT_OVERHEATING, False - ) - - if _overheating_protection is True: - if self.cur_temp >= self.bt_target_temp: - _calibrated_setpoint -= (self.cur_temp - self.bt_target_temp) * 10.0 - - _calibrated_setpoint = round_down_to_half_degree(_calibrated_setpoint) - - # check if new setpoint is inside the TRV's range, else set to min or max - if _calibrated_setpoint < self.real_trvs[entity_id]["min_temp"]: - _calibrated_setpoint = self.real_trvs[entity_id]["min_temp"] - if _calibrated_setpoint > self.real_trvs[entity_id]["max_temp"]: - _calibrated_setpoint = self.real_trvs[entity_id]["max_temp"] - - return _calibrated_setpoint - - def heating_power_valve_position(self, entity_id): _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp)) valve_pos = (_temp_diff / self.heating_power) / 100 @@ -343,6 +140,32 @@ def calibration_round(value: Union[int, float, None]) -> Union[float, int, None] else: return float(str(split[0])) +def round_by_steps( + value: Union[int, float, None], + steps: Union[int, float, None] +) -> Union[float, int, None]: + """Round the value based on the allowed decimal 'steps'. + + Parameters + ---------- + value : float + the value to round + + Returns + ------- + float + the rounded value + """ + if value is None: + return None + split = str(float(str(steps))).split(".", 1) + decimals = len(split[1]) + + value_f = float(str(value)) + steps_f = float(str(steps)) + value_mod = (value_f - (value_f % steps_f)) + + return round(value_mod, decimals) def round_down_to_half_degree( value: Union[int, float, None] From 88929247738124ea7c2f95782056361b4d6d29f6 Mon Sep 17 00:00:00 2001 From: xX7 <5272979+xX7@users.noreply.github.com> Date: Tue, 5 Dec 2023 21:45:06 +0000 Subject: [PATCH 2/3] Added debug logs to model_quirks.py Fixed model quirks of TS0601 --- .../better_thermostat/model_fixes/TS0601.py | 9 ++-- .../model_fixes/TS0601_thermostat.py | 9 ++-- .../model_fixes/model_quirks.py | 43 ++++++++++++++++++- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/custom_components/better_thermostat/model_fixes/TS0601.py b/custom_components/better_thermostat/model_fixes/TS0601.py index 8dc284a8..e43128aa 100644 --- a/custom_components/better_thermostat/model_fixes/TS0601.py +++ b/custom_components/better_thermostat/model_fixes/TS0601.py @@ -1,8 +1,11 @@ def fix_local_calibration(self, entity_id, offset): - if (self.cur_temp - 0.5) <= self.bt_target_temp: - offset -= 2.5 - elif (self.cur_temp + 0.10) >= self.bt_target_temp: + _cur_external_temp = self.cur_temp + _target_temp = self.bt_target_temp + + if (_cur_external_temp + 0.1) >= _target_temp: offset = round(offset + 0.5, 1) + elif (_cur_external_temp + 0.5) >= _target_temp: + offset -= 2.5 return offset diff --git a/custom_components/better_thermostat/model_fixes/TS0601_thermostat.py b/custom_components/better_thermostat/model_fixes/TS0601_thermostat.py index 8dc284a8..e43128aa 100644 --- a/custom_components/better_thermostat/model_fixes/TS0601_thermostat.py +++ b/custom_components/better_thermostat/model_fixes/TS0601_thermostat.py @@ -1,8 +1,11 @@ def fix_local_calibration(self, entity_id, offset): - if (self.cur_temp - 0.5) <= self.bt_target_temp: - offset -= 2.5 - elif (self.cur_temp + 0.10) >= self.bt_target_temp: + _cur_external_temp = self.cur_temp + _target_temp = self.bt_target_temp + + if (_cur_external_temp + 0.1) >= _target_temp: offset = round(offset + 0.5, 1) + elif (_cur_external_temp + 0.5) >= _target_temp: + offset -= 2.5 return offset diff --git a/custom_components/better_thermostat/model_fixes/model_quirks.py b/custom_components/better_thermostat/model_fixes/model_quirks.py index a1a938a5..f97e38fe 100644 --- a/custom_components/better_thermostat/model_fixes/model_quirks.py +++ b/custom_components/better_thermostat/model_fixes/model_quirks.py @@ -32,16 +32,55 @@ def load_model_quirks(self, model, entity_id): def fix_local_calibration(self, entity_id, offset): - return self.real_trvs[entity_id]["model_quirks"].fix_local_calibration( + """ Modifies the input local calibration offset, based on the TRV's model quirks, + to achieve the desired heating behavior. + + Returns + ------- + float + new local calibration offset, if the TRV model has any quirks/fixes. + """ + + _new_offset = self.real_trvs[entity_id]["model_quirks"].fix_local_calibration( self, entity_id, offset ) + if offset != _new_offset: + _LOGGER.debug( + "better_thermostat %s: %s - calibration offset model fix: %s to %s", + self.name, + entity_id, + offset, + _new_offset + ) + + return _new_offset def fix_target_temperature_calibration(self, entity_id, temperature): - return self.real_trvs[entity_id]["model_quirks"].fix_target_temperature_calibration( + """ Modifies the input setpoint temperature, based on the TRV's model quirks, + to achieve the desired heating behavior. + + Returns + ------- + float + new setpoint temperature, if the TRV model has any quirks/fixes. + """ + + _new_temperature = self.real_trvs[entity_id]["model_quirks"].fix_target_temperature_calibration( self, entity_id, temperature ) + if temperature != _new_temperature: + _LOGGER.debug( + "better_thermostat %s: %s - temperature offset model fix: %s to %s", + self.name, + entity_id, + temperature, + _new_temperature + ) + + return _new_temperature + async def override_set_hvac_mode(self, entity_id, hvac_mode): return await self.real_trvs[entity_id]["model_quirks"].override_set_hvac_mode( From 5675cf76a6221ea145f7ec8487b8c5a165dfd0fd Mon Sep 17 00:00:00 2001 From: xX7 <5272979+xX7@users.noreply.github.com> Date: Tue, 5 Dec 2023 22:59:58 +0000 Subject: [PATCH 3/3] Removed old_internal and old_external temperatur check/set from calibration, resulting in more eager calibration adjustment also on hvac_mode change --- .../better_thermostat/calibration.py | 18 +----------------- custom_components/better_thermostat/climate.py | 2 -- .../better_thermostat/events/trv.py | 2 -- .../better_thermostat/utils/controlling.py | 2 +- 4 files changed, 2 insertions(+), 22 deletions(-) diff --git a/custom_components/better_thermostat/calibration.py b/custom_components/better_thermostat/calibration.py index 5cb232a0..0f75b20c 100644 --- a/custom_components/better_thermostat/calibration.py +++ b/custom_components/better_thermostat/calibration.py @@ -42,34 +42,18 @@ def calculate_calibration_local(self, entity_id) -> Union[float, None]: """ _context = "_calculate_calibration_local()" - if None in (self.cur_temp, self.bt_target_temp, self.old_internal_temp): + if None in (self.cur_temp, self.bt_target_temp): return None - _old_trv_temp_s = self.old_internal_temp _cur_trv_temp_s = self.real_trvs[entity_id]["current_temperature"] - _calibration_steps = self.real_trvs[entity_id]["local_calibration_steps"] - - _old_external_temp = self.old_external_temp _cur_external_temp = self.cur_temp - _cur_target_temp = self.bt_target_temp _cur_trv_temp_f = convert_to_float( str(_cur_trv_temp_s), self.name, _context ) - # check if we need to calculate - if ( - _cur_trv_temp_s == _old_trv_temp_s - and _cur_external_temp == _old_external_temp - ): - return None - - # Setting old variables - self.old_internal_temp = _cur_trv_temp_s - self.old_external_temp = _cur_external_temp - _current_trv_calibration = convert_to_float( str(self.real_trvs[entity_id]["last_calibration"]), self.name, _context ) diff --git a/custom_components/better_thermostat/climate.py b/custom_components/better_thermostat/climate.py index 4e5c4018..8dea6079 100644 --- a/custom_components/better_thermostat/climate.py +++ b/custom_components/better_thermostat/climate.py @@ -284,8 +284,6 @@ def __init__( self.heating_end_temp = None self.heating_end_timestamp = None self._async_unsub_state_changed = None - self.old_external_temp = 0 - self.old_internal_temp = 0 self.all_entities = [] self.devices_states = {} self.devices_errors = [] diff --git a/custom_components/better_thermostat/events/trv.py b/custom_components/better_thermostat/events/trv.py index c6f0bbc5..e2779033 100644 --- a/custom_components/better_thermostat/events/trv.py +++ b/custom_components/better_thermostat/events/trv.py @@ -102,8 +102,6 @@ async def trigger_trv_change(self, event): f"better_thermostat {self.name}: calibration accepted by TRV {entity_id}" ) _main_change = False - self.old_internal_temp = self.real_trvs[entity_id]["current_temperature"] - self.old_external_temp = self.cur_temp if self.real_trvs[entity_id]["calibration"] == 0: self.real_trvs[entity_id][ "last_calibration" diff --git a/custom_components/better_thermostat/utils/controlling.py b/custom_components/better_thermostat/utils/controlling.py index a95100c3..e3092147 100644 --- a/custom_components/better_thermostat/utils/controlling.py +++ b/custom_components/better_thermostat/utils/controlling.py @@ -113,7 +113,7 @@ async def control_trv(self, heater_entity_id=None): _temperature = _remapped_states.get("temperature", None) _calibration = _remapped_states.get("local_temperature_calibration", None) _calibration_mode = self.real_trvs[heater_entity_id]["advanced"].get( - "calibration_mode", CalibrationMode.DEFAULT + "calibration_mode", CalibrationMode.DEFAULT ) _new_hvac_mode = handle_window_open(self, _remapped_states)