From 592b2ca5746946fd2eb52ccf2f81109599e677ca Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Tue, 19 Nov 2024 19:32:20 +0000 Subject: [PATCH] Fix hvac_action Fix offset_calibration=room_temp - (local_temp - current_offset) --- .../versatile_thermostat/const.py | 15 +++++ .../thermostat_sonoff_trvzb.py | 22 +++++-- .../versatile_thermostat/underlyings.py | 61 +++++++++++++------ 3 files changed, 74 insertions(+), 24 deletions(-) diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index 8f59636..ffb0db7 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -2,6 +2,7 @@ """Constants for the Versatile Thermostat integration.""" import logging +import math from typing import Literal from enum import Enum @@ -491,6 +492,20 @@ def send_vtherm_event(hass, event_type: EventType, entity, data: dict): hass.bus.fire(event_type.value, data) +def get_safe_float(hass, entity_id: str): + """Get a safe float state value for an entity. + Return None if entity is not available""" + if ( + entity_id is None + or not (state := hass.states.get(entity_id)) + or state.state == "unknown" + or state.state == "unavailable" + ): + return None + float_val = float(state.state) + return None if math.isinf(float_val) or not math.isfinite(float_val) else float_val + + class UnknownEntity(HomeAssistantError): """Error to indicate there is an unknown entity_id given.""" diff --git a/custom_components/versatile_thermostat/thermostat_sonoff_trvzb.py b/custom_components/versatile_thermostat/thermostat_sonoff_trvzb.py index ee15ed8..ac54c65 100644 --- a/custom_components/versatile_thermostat/thermostat_sonoff_trvzb.py +++ b/custom_components/versatile_thermostat/thermostat_sonoff_trvzb.py @@ -41,7 +41,7 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate): ) ) _underlyings_sonoff_trvzb: list[UnderlyingSonoffTRVZB] = [] - _valve_open_percent: int = 0 + _valve_open_percent: int | None = None _last_calculation_timestamp: datetime | None = None _auto_regulation_dpercent: float | None = None _auto_regulation_period_min: int | None = None @@ -188,9 +188,14 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate): if new_valve_percent < self._auto_regulation_dpercent: new_valve_percent = 0 - dpercent = new_valve_percent - self.valve_open_percent + dpercent = ( + new_valve_percent - self._valve_open_percent + if self._valve_open_percent is not None + else 0 + ) if ( - new_valve_percent > 0 + self._last_calculation_timestamp is not None + and new_valve_percent > 0 and -1 * self._auto_regulation_dpercent <= dpercent < self._auto_regulation_dpercent @@ -203,7 +208,10 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate): return - if self._valve_open_percent == new_valve_percent: + if ( + self._last_calculation_timestamp is not None + and self._valve_open_percent == new_valve_percent + ): _LOGGER.debug("%s - no change in valve_open_percent.", self) return @@ -216,6 +224,10 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate): self.update_custom_attributes() + async def _send_regulated_temperature(self, force=False): + """Sends the regulated temperature to all underlying""" + self.recalculate() + @property def is_over_sonoff_trvzb(self) -> bool: """True if the Thermostat is over_sonoff_trvzb""" @@ -237,7 +249,7 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate): @property def valve_open_percent(self) -> int: """Gives the percentage of valve needed""" - if self._hvac_mode == HVACMode.OFF: + if self._hvac_mode == HVACMode.OFF or self._valve_open_percent is None: return 0 else: return self._valve_open_percent diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py index 7688148..052df45 100644 --- a/custom_components/versatile_thermostat/underlyings.py +++ b/custom_components/versatile_thermostat/underlyings.py @@ -32,7 +32,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_call_later from homeassistant.util.unit_conversion import TemperatureConverter -from .const import UnknownEntity, overrides +from .const import UnknownEntity, overrides, get_safe_float from .keep_alive import IntervalCaller _LOGGER = logging.getLogger(__name__) @@ -884,7 +884,7 @@ class UnderlyingValve(UnderlyingEntity): self._async_cancel_cycle = None self._should_relaunch_control_heating = False self._hvac_mode = None - self._percent_open = self._thermostat.valve_open_percent + self._percent_open = None # self._thermostat.valve_open_percent self._valve_entity_id = valve_entity_id async def _send_value_to_number(self, number_entity_id: str, value: int): @@ -1062,6 +1062,7 @@ class UnderlyingSonoffTRVZB(UnderlyingValve): self._is_min_max_initialized = ( self._max_opening_degree is not None and self._min_offset_calibration is not None + and self._max_offset_calibration is not None ) if not self._is_min_max_initialized: @@ -1073,25 +1074,39 @@ class UnderlyingSonoffTRVZB(UnderlyingValve): # Send opening_degree await super().send_percent_open() - # Send closing_degree. - await self._send_value_to_number( - self._closing_degree_entity_id, - closing_degree := self._max_opening_degree - self._percent_open, - ) - - # send offset_calibration to the difference between target temp and local temp - offset = 0 - if ( - local_temp := self._climate_underlying.underlying_current_temperature - ) is not None and ( - room_temp := self._thermostat.current_temperature - ) is not None: - offset = min( - self._max_offset_calibration, - max(self._min_offset_calibration, room_temp - local_temp), + # Send closing_degree if set + closing_degree = None + if self._closing_degree_entity_id is not None: + await self._send_value_to_number( + self._closing_degree_entity_id, + closing_degree := self._max_opening_degree - self._percent_open, ) - await self._send_value_to_number(self._offset_calibration_entity_id, offset) + # send offset_calibration to the difference between target temp and local temp + offset = None + if self._offset_calibration_entity_id is not None: + if ( + (local_temp := self._climate_underlying.underlying_current_temperature) + is not None + and (room_temp := self._thermostat.current_temperature) is not None + and ( + current_offset := get_safe_float( + self._hass, self._offset_calibration_entity_id + ) + ) + is not None + ): + offset = min( + self._max_offset_calibration, + max( + self._min_offset_calibration, + room_temp - (local_temp - current_offset), + ), + ) + + await self._send_value_to_number( + self._offset_calibration_entity_id, offset + ) _LOGGER.debug( "%s - SonoffTRVZB - I have sent offset_calibration=%s opening_degree=%s closing_degree=%s", @@ -1122,3 +1137,11 @@ class UnderlyingSonoffTRVZB(UnderlyingValve): if not self.is_initialized: return [] return [HVACMode.OFF, HVACMode.HEAT] + + @property + def is_device_active(self): + """If the opening valve is open.""" + try: + return get_safe_float(self._hass, self._opening_degree_entity_id) > 0 + except Exception: # pylint: disable=broad-exception-caught + return False