Compare commits
2 Commits
6.8.0.beta
...
6.8.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
592b2ca574 | ||
|
|
9102b09691 |
@@ -504,7 +504,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
entry_infos.get(CONF_WINDOW_ACTION) or CONF_WINDOW_TURN_OFF
|
entry_infos.get(CONF_WINDOW_ACTION) or CONF_WINDOW_TURN_OFF
|
||||||
)
|
)
|
||||||
|
|
||||||
self._max_on_percent = api._max_on_percent
|
self._max_on_percent = api.max_on_percent
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s - Creation of a new VersatileThermostat entity: unique_id=%s",
|
"%s - Creation of a new VersatileThermostat entity: unique_id=%s",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"""Constants for the Versatile Thermostat integration."""
|
"""Constants for the Versatile Thermostat integration."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@@ -464,9 +465,9 @@ class RegulationParamVeryStrong:
|
|||||||
kp: float = 0.6
|
kp: float = 0.6
|
||||||
ki: float = 0.1
|
ki: float = 0.1
|
||||||
k_ext: float = 0.2
|
k_ext: float = 0.2
|
||||||
offset_max: float = 4
|
offset_max: float = 8
|
||||||
stabilization_threshold: float = 0.1
|
stabilization_threshold: float = 0.1
|
||||||
accumulated_error_threshold: float = 30
|
accumulated_error_threshold: float = 80
|
||||||
|
|
||||||
|
|
||||||
class EventType(Enum):
|
class EventType(Enum):
|
||||||
@@ -491,6 +492,20 @@ def send_vtherm_event(hass, event_type: EventType, entity, data: dict):
|
|||||||
hass.bus.fire(event_type.value, data)
|
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):
|
class UnknownEntity(HomeAssistantError):
|
||||||
"""Error to indicate there is an unknown entity_id given."""
|
"""Error to indicate there is an unknown entity_id given."""
|
||||||
|
|
||||||
|
|||||||
@@ -151,15 +151,13 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
"""True if the Thermostat is over_climate"""
|
"""True if the Thermostat is over_climate"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
def calculate_hvac_action(self, under_list: list) -> HVACAction | None:
|
||||||
def hvac_action(self) -> HVACAction | None:
|
"""Calculate an hvac action based on the hvac_action of the list in argument"""
|
||||||
"""Returns the current hvac_action by checking all hvac_action of the underlyings"""
|
|
||||||
|
|
||||||
# if one not IDLE or OFF -> return it
|
# if one not IDLE or OFF -> return it
|
||||||
# else if one IDLE -> IDLE
|
# else if one IDLE -> IDLE
|
||||||
# else OFF
|
# else OFF
|
||||||
one_idle = False
|
one_idle = False
|
||||||
for under in self._underlyings:
|
for under in under_list:
|
||||||
if (action := under.hvac_action) not in [
|
if (action := under.hvac_action) not in [
|
||||||
HVACAction.IDLE,
|
HVACAction.IDLE,
|
||||||
HVACAction.OFF,
|
HVACAction.OFF,
|
||||||
@@ -171,6 +169,11 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
return HVACAction.IDLE
|
return HVACAction.IDLE
|
||||||
return HVACAction.OFF
|
return HVACAction.OFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hvac_action(self) -> HVACAction | None:
|
||||||
|
"""Returns the current hvac_action by checking all hvac_action of the underlyings"""
|
||||||
|
return self.calculate_hvac_action(self._underlyings)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
async def _async_internal_set_temperature(self, temperature: float):
|
async def _async_internal_set_temperature(self, temperature: float):
|
||||||
"""Set the target temperature and the target temperature of underlying climate if any"""
|
"""Set the target temperature and the target temperature of underlying climate if any"""
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import logging
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.components.climate import HVACMode
|
from homeassistant.components.climate import HVACMode, HVACAction
|
||||||
|
|
||||||
from .underlyings import UnderlyingSonoffTRVZB
|
from .underlyings import UnderlyingSonoffTRVZB
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
_underlyings_sonoff_trvzb: list[UnderlyingSonoffTRVZB] = []
|
_underlyings_sonoff_trvzb: list[UnderlyingSonoffTRVZB] = []
|
||||||
_valve_open_percent: int = 0
|
_valve_open_percent: int | None = None
|
||||||
_last_calculation_timestamp: datetime | None = None
|
_last_calculation_timestamp: datetime | None = None
|
||||||
_auto_regulation_dpercent: float | None = None
|
_auto_regulation_dpercent: float | None = None
|
||||||
_auto_regulation_period_min: int | None = None
|
_auto_regulation_period_min: int | None = None
|
||||||
@@ -96,6 +96,7 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate):
|
|||||||
offset_calibration_entity_id=offset,
|
offset_calibration_entity_id=offset,
|
||||||
opening_degree_entity_id=opening,
|
opening_degree_entity_id=opening,
|
||||||
closing_degree_entity_id=closing,
|
closing_degree_entity_id=closing,
|
||||||
|
climate_underlying=self._underlyings[idx],
|
||||||
)
|
)
|
||||||
self._underlyings_sonoff_trvzb.append(under)
|
self._underlyings_sonoff_trvzb.append(under)
|
||||||
|
|
||||||
@@ -187,9 +188,14 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate):
|
|||||||
if new_valve_percent < self._auto_regulation_dpercent:
|
if new_valve_percent < self._auto_regulation_dpercent:
|
||||||
new_valve_percent = 0
|
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 (
|
if (
|
||||||
new_valve_percent > 0
|
self._last_calculation_timestamp is not None
|
||||||
|
and new_valve_percent > 0
|
||||||
and -1 * self._auto_regulation_dpercent
|
and -1 * self._auto_regulation_dpercent
|
||||||
<= dpercent
|
<= dpercent
|
||||||
< self._auto_regulation_dpercent
|
< self._auto_regulation_dpercent
|
||||||
@@ -202,7 +208,10 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate):
|
|||||||
|
|
||||||
return
|
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)
|
_LOGGER.debug("%s - no change in valve_open_percent.", self)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -215,6 +224,10 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate):
|
|||||||
|
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
|
|
||||||
|
async def _send_regulated_temperature(self, force=False):
|
||||||
|
"""Sends the regulated temperature to all underlying"""
|
||||||
|
self.recalculate()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_over_sonoff_trvzb(self) -> bool:
|
def is_over_sonoff_trvzb(self) -> bool:
|
||||||
"""True if the Thermostat is over_sonoff_trvzb"""
|
"""True if the Thermostat is over_sonoff_trvzb"""
|
||||||
@@ -236,7 +249,13 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate):
|
|||||||
@property
|
@property
|
||||||
def valve_open_percent(self) -> int:
|
def valve_open_percent(self) -> int:
|
||||||
"""Gives the percentage of valve needed"""
|
"""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
|
return 0
|
||||||
else:
|
else:
|
||||||
return self._valve_open_percent
|
return self._valve_open_percent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hvac_action(self) -> HVACAction | None:
|
||||||
|
"""Returns the current hvac_action by checking all hvac_action of the _underlyings_sonoff_trvzb"""
|
||||||
|
|
||||||
|
return self.calculate_hvac_action(self._underlyings_sonoff_trvzb)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from homeassistant.helpers.entity_component import EntityComponent
|
|||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
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
|
from .keep_alive import IntervalCaller
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -884,7 +884,7 @@ class UnderlyingValve(UnderlyingEntity):
|
|||||||
self._async_cancel_cycle = None
|
self._async_cancel_cycle = None
|
||||||
self._should_relaunch_control_heating = False
|
self._should_relaunch_control_heating = False
|
||||||
self._hvac_mode = None
|
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
|
self._valve_entity_id = valve_entity_id
|
||||||
|
|
||||||
async def _send_value_to_number(self, number_entity_id: str, value: int):
|
async def _send_value_to_number(self, number_entity_id: str, value: int):
|
||||||
@@ -1030,15 +1030,18 @@ class UnderlyingSonoffTRVZB(UnderlyingValve):
|
|||||||
offset_calibration_entity_id: str,
|
offset_calibration_entity_id: str,
|
||||||
opening_degree_entity_id: str,
|
opening_degree_entity_id: str,
|
||||||
closing_degree_entity_id: str,
|
closing_degree_entity_id: str,
|
||||||
|
climate_underlying: UnderlyingClimate,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the underlying Sonoff TRV"""
|
"""Initialize the underlying Sonoff TRV"""
|
||||||
super().__init__(hass, thermostat, opening_degree_entity_id)
|
super().__init__(hass, thermostat, opening_degree_entity_id)
|
||||||
self._offset_calibration_entity_id = offset_calibration_entity_id
|
self._offset_calibration_entity_id = offset_calibration_entity_id
|
||||||
self._opening_degree_entity_id = opening_degree_entity_id
|
self._opening_degree_entity_id = opening_degree_entity_id
|
||||||
self._closing_degree_entity_id = closing_degree_entity_id
|
self._closing_degree_entity_id = closing_degree_entity_id
|
||||||
|
self._climate_underlying = climate_underlying
|
||||||
self._is_min_max_initialized = False
|
self._is_min_max_initialized = False
|
||||||
self._max_opening_degree = None
|
self._max_opening_degree = None
|
||||||
self._min_offset_calibration = None
|
self._min_offset_calibration = None
|
||||||
|
self._max_offset_calibration = None
|
||||||
|
|
||||||
async def send_percent_open(self):
|
async def send_percent_open(self):
|
||||||
"""Send the percent open to the underlying valve"""
|
"""Send the percent open to the underlying valve"""
|
||||||
@@ -1052,10 +1055,14 @@ class UnderlyingSonoffTRVZB(UnderlyingValve):
|
|||||||
self._min_offset_calibration = self._hass.states.get(
|
self._min_offset_calibration = self._hass.states.get(
|
||||||
self._offset_calibration_entity_id
|
self._offset_calibration_entity_id
|
||||||
).attributes.get("min")
|
).attributes.get("min")
|
||||||
|
self._max_offset_calibration = self._hass.states.get(
|
||||||
|
self._offset_calibration_entity_id
|
||||||
|
).attributes.get("max")
|
||||||
|
|
||||||
self._is_min_max_initialized = (
|
self._is_min_max_initialized = (
|
||||||
self._max_opening_degree is not None
|
self._max_opening_degree is not None
|
||||||
and self._min_offset_calibration 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:
|
if not self._is_min_max_initialized:
|
||||||
@@ -1067,15 +1074,46 @@ class UnderlyingSonoffTRVZB(UnderlyingValve):
|
|||||||
# Send opening_degree
|
# Send opening_degree
|
||||||
await super().send_percent_open()
|
await super().send_percent_open()
|
||||||
|
|
||||||
# Send closing_degree. TODO 100 hard-coded or take the max of the _closing_degree_entity_id ?
|
# Send closing_degree if set
|
||||||
await self._send_value_to_number(
|
closing_degree = None
|
||||||
self._closing_degree_entity_id,
|
if self._closing_degree_entity_id is not None:
|
||||||
self._max_opening_degree - self._percent_open,
|
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 min value
|
# send offset_calibration to the difference between target temp and local temp
|
||||||
await self._send_value_to_number(
|
offset = None
|
||||||
self._offset_calibration_entity_id, self._min_offset_calibration
|
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",
|
||||||
|
self,
|
||||||
|
offset,
|
||||||
|
self._percent_open,
|
||||||
|
closing_degree,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1099,3 +1137,11 @@ class UnderlyingSonoffTRVZB(UnderlyingValve):
|
|||||||
if not self.is_initialized:
|
if not self.is_initialized:
|
||||||
return []
|
return []
|
||||||
return [HVACMode.OFF, HVACMode.HEAT]
|
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
|
||||||
|
|||||||
@@ -179,7 +179,8 @@ class VersatileThermostatAPI(dict):
|
|||||||
# ):
|
# ):
|
||||||
# await entity.init_presets(self.find_central_configuration())
|
# await entity.init_presets(self.find_central_configuration())
|
||||||
|
|
||||||
# A little hack to test if the climate is a VTherm. Cannot use isinstance due to circular dependency of BaseThermostat
|
# A little hack to test if the climate is a VTherm. Cannot use isinstance
|
||||||
|
# due to circular dependency of BaseThermostat
|
||||||
if (
|
if (
|
||||||
entity.device_info
|
entity.device_info
|
||||||
and entity.device_info.get("model", None) == DOMAIN
|
and entity.device_info.get("model", None) == DOMAIN
|
||||||
@@ -249,6 +250,11 @@ class VersatileThermostatAPI(dict):
|
|||||||
"""Get the safety_mode params"""
|
"""Get the safety_mode params"""
|
||||||
return self._safety_mode
|
return self._safety_mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_on_percent(self):
|
||||||
|
"""Get the max_open_percent params"""
|
||||||
|
return self._max_on_percent
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def central_boiler_entity(self):
|
def central_boiler_entity(self):
|
||||||
"""Get the central boiler binary_sensor entity"""
|
"""Get the central boiler binary_sensor entity"""
|
||||||
|
|||||||
Reference in New Issue
Block a user