Refactorisation and complete #137
This commit is contained in:
@@ -411,15 +411,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# Initiate the ProportionalAlgorithm
|
# Initiate the ProportionalAlgorithm
|
||||||
if self._prop_algorithm is not None:
|
if self._prop_algorithm is not None:
|
||||||
del self._prop_algorithm
|
del self._prop_algorithm
|
||||||
if not self.is_over_climate:
|
|
||||||
self._prop_algorithm = PropAlgorithm(
|
|
||||||
self._proportional_function,
|
|
||||||
self._tpi_coef_int,
|
|
||||||
self._tpi_coef_ext,
|
|
||||||
self._cycle_min,
|
|
||||||
self._minimal_activation_delay,
|
|
||||||
)
|
|
||||||
self._should_relaunch_control_heating = False
|
|
||||||
|
|
||||||
# Memory synthesis state
|
# Memory synthesis state
|
||||||
self._motion_state = None
|
self._motion_state = None
|
||||||
@@ -677,7 +668,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._hvac_mode == HVACMode.COOL,
|
self._hvac_mode == HVACMode.COOL,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.hass.create_task(self._check_switch_initial_state())
|
self.hass.create_task(self._check_initial_state())
|
||||||
|
|
||||||
self.reset_last_change_time()
|
self.reset_last_change_time()
|
||||||
|
|
||||||
@@ -820,9 +811,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.FAN_MODE.
|
Requires ClimateEntityFeature.FAN_MODE.
|
||||||
"""
|
"""
|
||||||
if self.is_over_climate and self.underlying_entity(0):
|
|
||||||
return self.underlying_entity(0).fan_mode
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -831,9 +819,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.FAN_MODE.
|
Requires ClimateEntityFeature.FAN_MODE.
|
||||||
"""
|
"""
|
||||||
if self.is_over_climate and self.underlying_entity(0):
|
|
||||||
return self.underlying_entity(0).fan_modes
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -842,9 +827,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.SWING_MODE.
|
Requires ClimateEntityFeature.SWING_MODE.
|
||||||
"""
|
"""
|
||||||
if self.is_over_climate and self.underlying_entity(0):
|
|
||||||
return self.underlying_entity(0).swing_mode
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -853,17 +835,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.SWING_MODE.
|
Requires ClimateEntityFeature.SWING_MODE.
|
||||||
"""
|
"""
|
||||||
if self.is_over_climate and self.underlying_entity(0):
|
|
||||||
return self.underlying_entity(0).swing_modes
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temperature_unit(self) -> str:
|
def temperature_unit(self) -> str:
|
||||||
"""Return the unit of measurement."""
|
"""Return the unit of measurement."""
|
||||||
if self.is_over_climate and self.underlying_entity(0):
|
|
||||||
return self.underlying_entity(0).temperature_unit
|
|
||||||
|
|
||||||
return self._unit
|
return self._unit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -904,9 +880,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
if self.is_over_climate and self.underlying_entity(0):
|
|
||||||
return self.underlying_entity(0).supported_features | self._support_flags
|
|
||||||
|
|
||||||
return self._support_flags
|
return self._support_flags
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -925,9 +898,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
@property
|
@property
|
||||||
def target_temperature_step(self) -> float | None:
|
def target_temperature_step(self) -> float | None:
|
||||||
"""Return the supported step of target temperature."""
|
"""Return the supported step of target temperature."""
|
||||||
if self.is_over_climate and self.underlying_entity(0):
|
|
||||||
return self.underlying_entity(0).target_temperature_step
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -936,9 +906,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||||
"""
|
"""
|
||||||
if self.is_over_climate and self.underlying_entity(0):
|
|
||||||
return self.underlying_entity(0).target_temperature_high
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -947,9 +914,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||||
"""
|
"""
|
||||||
if self.is_over_climate and self.underlying_entity(0):
|
|
||||||
return self.underlying_entity(0).target_temperature_low
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -958,15 +922,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.AUX_HEAT.
|
Requires ClimateEntityFeature.AUX_HEAT.
|
||||||
"""
|
"""
|
||||||
if self.is_over_climate and self.underlying_entity(0):
|
|
||||||
return self.underlying_entity(0).is_aux_heat
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mean_cycle_power(self) -> float | None:
|
def mean_cycle_power(self) -> float | None:
|
||||||
"""Returns the mean power consumption during the cycle"""
|
"""Returns the mean power consumption during the cycle"""
|
||||||
if not self._device_power or self.is_over_climate:
|
if not self._device_power:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return float(
|
return float(
|
||||||
@@ -978,7 +939,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
@property
|
@property
|
||||||
def total_energy(self) -> float | None:
|
def total_energy(self) -> float | None:
|
||||||
"""Returns the total energy calculated for this thermostast"""
|
"""Returns the total energy calculated for this thermostast"""
|
||||||
return self._total_energy
|
return round(self._total_energy, 2)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_power(self) -> float | None:
|
def device_power(self) -> float | None:
|
||||||
@@ -1080,33 +1041,18 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
def turn_aux_heat_on(self) -> None:
|
def turn_aux_heat_on(self) -> None:
|
||||||
"""Turn auxiliary heater on."""
|
"""Turn auxiliary heater on."""
|
||||||
if self.is_over_climate and self.underlying_entity(0):
|
|
||||||
return self.underlying_entity(0).turn_aux_heat_on()
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def async_turn_aux_heat_on(self) -> None:
|
async def async_turn_aux_heat_on(self) -> None:
|
||||||
"""Turn auxiliary heater on."""
|
"""Turn auxiliary heater on."""
|
||||||
if self.is_over_climate:
|
|
||||||
for under in self._underlyings:
|
|
||||||
await under.async_turn_aux_heat_on()
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def turn_aux_heat_off(self) -> None:
|
def turn_aux_heat_off(self) -> None:
|
||||||
"""Turn auxiliary heater off."""
|
"""Turn auxiliary heater off."""
|
||||||
if self.is_over_climate:
|
|
||||||
for under in self._underlyings:
|
|
||||||
return under.turn_aux_heat_off()
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def async_turn_aux_heat_off(self) -> None:
|
async def async_turn_aux_heat_off(self) -> None:
|
||||||
"""Turn auxiliary heater off."""
|
"""Turn auxiliary heater off."""
|
||||||
if self.is_over_climate:
|
|
||||||
for under in self._underlyings:
|
|
||||||
await under.async_turn_aux_heat_off()
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode, need_control_heating=True):
|
async def async_set_hvac_mode(self, hvac_mode, need_control_heating=True):
|
||||||
@@ -1239,33 +1185,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
async def async_set_fan_mode(self, fan_mode):
|
async def async_set_fan_mode(self, fan_mode):
|
||||||
"""Set new target fan mode."""
|
"""Set new target fan mode."""
|
||||||
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
|
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
|
||||||
if fan_mode is None or not self.is_over_climate:
|
return
|
||||||
return
|
|
||||||
|
|
||||||
for under in self._underlyings:
|
|
||||||
await under.set_fan_mode(fan_mode)
|
|
||||||
self._fan_mode = fan_mode
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
async def async_set_humidity(self, humidity: int):
|
async def async_set_humidity(self, humidity: int):
|
||||||
"""Set new target humidity."""
|
"""Set new target humidity."""
|
||||||
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
|
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
|
||||||
if humidity is None or not self.is_over_climate:
|
return
|
||||||
return
|
|
||||||
for under in self._underlyings:
|
|
||||||
await under.set_humidity(humidity)
|
|
||||||
self._humidity = humidity
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
async def async_set_swing_mode(self, swing_mode):
|
async def async_set_swing_mode(self, swing_mode):
|
||||||
"""Set new target swing operation."""
|
"""Set new target swing operation."""
|
||||||
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
|
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
|
||||||
if swing_mode is None or not self.is_over_climate:
|
return
|
||||||
return
|
|
||||||
for under in self._underlyings:
|
|
||||||
await under.set_swing_mode(swing_mode)
|
|
||||||
self._swing_mode = swing_mode
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs):
|
async def async_set_temperature(self, **kwargs):
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
@@ -1282,13 +1212,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
async def _async_internal_set_temperature(self, temperature):
|
async def _async_internal_set_temperature(self, temperature):
|
||||||
"""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"""
|
||||||
self._target_temp = temperature
|
self._target_temp = temperature
|
||||||
if not self.is_over_climate:
|
return
|
||||||
return
|
|
||||||
|
|
||||||
for under in self._underlyings:
|
|
||||||
await under.set_temperature(
|
|
||||||
temperature, self._attr_max_temp, self._attr_min_temp
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_state_date_or_now(self, state: State):
|
def get_state_date_or_now(self, state: State):
|
||||||
"""Extract the last_changed state from State or return now if not available"""
|
"""Extract the last_changed state from State or return now if not available"""
|
||||||
@@ -1520,197 +1444,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def _check_switch_initial_state(self):
|
async def _check_initial_state(self):
|
||||||
"""Prevent the device from keep running if HVAC_MODE_OFF."""
|
"""Prevent the device from keep running if HVAC_MODE_OFF."""
|
||||||
_LOGGER.debug("%s - Calling _check_switch_initial_state", self)
|
_LOGGER.debug("%s - Calling _check_initial_state", self)
|
||||||
# We need to do the same check for over_climate underlyings
|
|
||||||
# if self.is_over_climate:
|
|
||||||
# return
|
|
||||||
for under in self._underlyings:
|
for under in self._underlyings:
|
||||||
await under.check_initial_state(self._hvac_mode)
|
await under.check_initial_state(self._hvac_mode)
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_switch_changed(self, event):
|
|
||||||
"""Handle heater switch state changes."""
|
|
||||||
new_state = event.data.get("new_state")
|
|
||||||
old_state = event.data.get("old_state")
|
|
||||||
if new_state is None:
|
|
||||||
return
|
|
||||||
if old_state is None:
|
|
||||||
self.hass.create_task(self._check_switch_initial_state())
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
async def _async_climate_changed(self, event):
|
|
||||||
"""Handle unerdlying climate state changes.
|
|
||||||
This method takes the underlying values and update the VTherm with them.
|
|
||||||
To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received
|
|
||||||
less than 10 sec after the last command. What we want here is to take the values
|
|
||||||
from underlyings ONLY if someone have change directly on the underlying and not
|
|
||||||
as a return of the command. The only thing we take all the time is the HVACAction
|
|
||||||
which is important for feedaback and which cannot generates loops.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def end_climate_changed(changes):
|
|
||||||
"""To end the event management"""
|
|
||||||
if changes:
|
|
||||||
self.async_write_ha_state()
|
|
||||||
self.update_custom_attributes()
|
|
||||||
await self.async_control_heating()
|
|
||||||
|
|
||||||
new_state = event.data.get("new_state")
|
|
||||||
_LOGGER.debug("%s - _async_climate_changed new_state is %s", self, new_state)
|
|
||||||
if not new_state:
|
|
||||||
return
|
|
||||||
|
|
||||||
changes = False
|
|
||||||
new_hvac_mode = new_state.state
|
|
||||||
|
|
||||||
old_state = event.data.get("old_state")
|
|
||||||
old_hvac_action = (
|
|
||||||
old_state.attributes.get("hvac_action")
|
|
||||||
if old_state and old_state.attributes
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
new_hvac_action = (
|
|
||||||
new_state.attributes.get("hvac_action")
|
|
||||||
if new_state and new_state.attributes
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
old_state_date_changed = (
|
|
||||||
old_state.last_changed if old_state and old_state.last_changed else None
|
|
||||||
)
|
|
||||||
old_state_date_updated = (
|
|
||||||
old_state.last_updated if old_state and old_state.last_updated else None
|
|
||||||
)
|
|
||||||
new_state_date_changed = (
|
|
||||||
new_state.last_changed if new_state and new_state.last_changed else None
|
|
||||||
)
|
|
||||||
new_state_date_updated = (
|
|
||||||
new_state.last_updated if new_state and new_state.last_updated else None
|
|
||||||
)
|
|
||||||
|
|
||||||
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
|
|
||||||
# Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is
|
|
||||||
# if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE:
|
|
||||||
# _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
|
|
||||||
# new_hvac_mode = HVACMode.OFF
|
|
||||||
|
|
||||||
_LOGGER.info(
|
|
||||||
"%s - Underlying climate changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
|
|
||||||
self,
|
|
||||||
new_hvac_mode,
|
|
||||||
self._hvac_mode,
|
|
||||||
new_hvac_action,
|
|
||||||
old_hvac_action,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
|
||||||
"%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s",
|
|
||||||
self,
|
|
||||||
self._last_change_time,
|
|
||||||
old_state_date_changed,
|
|
||||||
old_state_date_updated,
|
|
||||||
new_state_date_changed,
|
|
||||||
new_state_date_updated,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Interpretation of hvac action
|
|
||||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
|
||||||
HVACAction.COOLING,
|
|
||||||
HVACAction.DRYING,
|
|
||||||
HVACAction.FAN,
|
|
||||||
HVACAction.HEATING,
|
|
||||||
]
|
|
||||||
if old_hvac_action not in HVAC_ACTION_ON and new_hvac_action in HVAC_ACTION_ON:
|
|
||||||
self._underlying_climate_start_hvac_action_date = (
|
|
||||||
self.get_last_updated_date_or_now(new_state)
|
|
||||||
)
|
|
||||||
_LOGGER.info(
|
|
||||||
"%s - underlying just switch ON. Set power and energy start date %s",
|
|
||||||
self,
|
|
||||||
self._underlying_climate_start_hvac_action_date.isoformat(),
|
|
||||||
)
|
|
||||||
changes = True
|
|
||||||
|
|
||||||
if old_hvac_action in HVAC_ACTION_ON and new_hvac_action not in HVAC_ACTION_ON:
|
|
||||||
stop_power_date = self.get_last_updated_date_or_now(new_state)
|
|
||||||
if self._underlying_climate_start_hvac_action_date:
|
|
||||||
delta = (
|
|
||||||
stop_power_date - self._underlying_climate_start_hvac_action_date
|
|
||||||
)
|
|
||||||
self._underlying_climate_delta_t = delta.total_seconds() / 3600.0
|
|
||||||
|
|
||||||
# increment energy at the end of the cycle
|
|
||||||
self.incremente_energy()
|
|
||||||
|
|
||||||
self._underlying_climate_start_hvac_action_date = None
|
|
||||||
|
|
||||||
_LOGGER.info(
|
|
||||||
"%s - underlying just switch OFF at %s. delta_h=%.3f h",
|
|
||||||
self,
|
|
||||||
stop_power_date.isoformat(),
|
|
||||||
self._underlying_climate_delta_t,
|
|
||||||
)
|
|
||||||
changes = True
|
|
||||||
|
|
||||||
# Issue #120 - Some TRV are chaning target temperature a very long time (6 sec) after the change.
|
|
||||||
# In that case a loop is possible if a user change multiple times during this 6 sec.
|
|
||||||
if new_state_date_updated and self._last_change_time:
|
|
||||||
delta = (new_state_date_updated - self._last_change_time).total_seconds()
|
|
||||||
if delta < 10:
|
|
||||||
_LOGGER.info(
|
|
||||||
"%s - underlying event is received less than 10 sec after command. Forget it to avoid loop",
|
|
||||||
self,
|
|
||||||
)
|
|
||||||
await end_climate_changed(changes)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (
|
|
||||||
new_hvac_mode
|
|
||||||
in [
|
|
||||||
HVACMode.OFF,
|
|
||||||
HVACMode.HEAT,
|
|
||||||
HVACMode.COOL,
|
|
||||||
HVACMode.HEAT_COOL,
|
|
||||||
HVACMode.DRY,
|
|
||||||
HVACMode.AUTO,
|
|
||||||
HVACMode.FAN_ONLY,
|
|
||||||
None,
|
|
||||||
]
|
|
||||||
and self._hvac_mode != new_hvac_mode
|
|
||||||
):
|
|
||||||
changes = True
|
|
||||||
self._hvac_mode = new_hvac_mode
|
|
||||||
# Update all underlyings state
|
|
||||||
if self.is_over_climate:
|
|
||||||
for under in self._underlyings:
|
|
||||||
await under.set_hvac_mode(new_hvac_mode)
|
|
||||||
|
|
||||||
if not changes:
|
|
||||||
# try to manage new target temperature set if state
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Do temperature check. temperature is %s, new_state.attributes is %s",
|
|
||||||
self.target_temperature,
|
|
||||||
new_state.attributes,
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
self.is_over_climate
|
|
||||||
and new_state.attributes
|
|
||||||
and (new_target_temp := new_state.attributes.get("temperature"))
|
|
||||||
and new_target_temp != self.target_temperature
|
|
||||||
):
|
|
||||||
_LOGGER.info(
|
|
||||||
"%s - Target temp in underlying have change to %s",
|
|
||||||
self,
|
|
||||||
new_target_temp,
|
|
||||||
)
|
|
||||||
await self.async_set_temperature(temperature=new_target_temp)
|
|
||||||
changes = True
|
|
||||||
|
|
||||||
await end_climate_changed(changes)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def _async_update_temp(self, state: State):
|
async def _async_update_temp(self, state: State):
|
||||||
"""Update thermostat with latest state from sensor."""
|
"""Update thermostat with latest state from sensor."""
|
||||||
@@ -2029,7 +1768,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
async def restore_hvac_mode(self, need_control_heating=False):
|
async def restore_hvac_mode(self, need_control_heating=False):
|
||||||
"""Restore a previous hvac_mod"""
|
"""Restore a previous hvac_mod"""
|
||||||
old_hvac_mode = self.hvac_mode
|
|
||||||
await self.async_set_hvac_mode(self._saved_hvac_mode, need_control_heating)
|
await self.async_set_hvac_mode(self._saved_hvac_mode, need_control_heating)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s - Restored hvac_mode - saved_hvac_mode is %s, hvac_mode is %s",
|
"%s - Restored hvac_mode - saved_hvac_mode is %s, hvac_mode is %s",
|
||||||
@@ -2037,16 +1775,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._saved_hvac_mode,
|
self._saved_hvac_mode,
|
||||||
self._hvac_mode,
|
self._hvac_mode,
|
||||||
)
|
)
|
||||||
# Issue 133 - force the temperature in over_climate mode if unerlying are turned on
|
|
||||||
if (
|
|
||||||
old_hvac_mode == HVACMode.OFF
|
|
||||||
and self.hvac_mode != HVACMode.OFF
|
|
||||||
and self.is_over_climate
|
|
||||||
):
|
|
||||||
_LOGGER.info(
|
|
||||||
"%s - force resent target temp cause we turn on some over climate"
|
|
||||||
)
|
|
||||||
await self._async_internal_set_temperature(self._target_temp)
|
|
||||||
|
|
||||||
async def check_overpowering(self) -> bool:
|
async def check_overpowering(self) -> bool:
|
||||||
"""Check the overpowering condition
|
"""Check the overpowering condition
|
||||||
@@ -2333,38 +2061,16 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
def recalculate(self):
|
def recalculate(self):
|
||||||
"""A utility function to force the calculation of a the algo and
|
"""A utility function to force the calculation of a the algo and
|
||||||
update the custom attributes and write the state
|
update the custom attributes and write the state.
|
||||||
|
Should be overriden by super class
|
||||||
"""
|
"""
|
||||||
_LOGGER.debug("%s - recalculate all", self)
|
raise NotImplementedError()
|
||||||
if not self.is_over_climate:
|
|
||||||
self._prop_algorithm.calculate(
|
|
||||||
self._target_temp,
|
|
||||||
self._cur_temp,
|
|
||||||
self._cur_ext_temp,
|
|
||||||
self._hvac_mode == HVACMode.COOL,
|
|
||||||
)
|
|
||||||
self.update_custom_attributes()
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
def incremente_energy(self):
|
def incremente_energy(self):
|
||||||
"""increment the energy counter if device is active"""
|
"""increment the energy counter if device is active
|
||||||
if self.hvac_mode == HVACMode.OFF:
|
Should be overriden by super class
|
||||||
return
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
added_energy = 0
|
|
||||||
if self.is_over_climate and self._underlying_climate_delta_t is not None:
|
|
||||||
added_energy = self._device_power * self._underlying_climate_delta_t
|
|
||||||
|
|
||||||
if not self.is_over_climate and self.mean_cycle_power is not None:
|
|
||||||
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
|
|
||||||
|
|
||||||
self._total_energy += added_energy
|
|
||||||
_LOGGER.debug(
|
|
||||||
"%s - added energy is %.3f . Total energy is now: %.3f",
|
|
||||||
self,
|
|
||||||
added_energy,
|
|
||||||
self._total_energy,
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_custom_attributes(self):
|
def update_custom_attributes(self):
|
||||||
"""Update the custom extra attributes for the entity"""
|
"""Update the custom extra attributes for the entity"""
|
||||||
@@ -2387,8 +2093,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self.get_preset_away_name(PRESET_COMFORT)
|
self.get_preset_away_name(PRESET_COMFORT)
|
||||||
),
|
),
|
||||||
"power_temp": self._power_temp,
|
"power_temp": self._power_temp,
|
||||||
"target_temp": self.target_temperature,
|
# Already in super class - "target_temp": self.target_temperature,
|
||||||
"current_temp": self._cur_temp,
|
# Already in super class - "current_temp": self._cur_temp,
|
||||||
"ext_current_temperature": self._cur_ext_temp,
|
"ext_current_temperature": self._cur_ext_temp,
|
||||||
"ac_mode": self._ac_mode,
|
"ac_mode": self._ac_mode,
|
||||||
"current_power": self._current_power,
|
"current_power": self._current_power,
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
||||||
|
|
||||||
from homeassistant.components.climate import HVACAction
|
from homeassistant.components.climate import HVACAction, HVACMode
|
||||||
|
|
||||||
from .base_thermostat import BaseThermostat
|
from .base_thermostat import BaseThermostat
|
||||||
|
|
||||||
from .const import CONF_CLIMATE, CONF_CLIMATE_2, CONF_CLIMATE_3, CONF_CLIMATE_4
|
from .const import CONF_CLIMATE, CONF_CLIMATE_2, CONF_CLIMATE_3, CONF_CLIMATE_4, overrides
|
||||||
|
|
||||||
from .underlyings import UnderlyingClimate
|
from .underlyings import UnderlyingClimate
|
||||||
|
|
||||||
@@ -25,9 +25,10 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
"underlying_climate_2", "underlying_climate_3"
|
"underlying_climate_2", "underlying_climate_3"
|
||||||
}))
|
}))
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
# Useless for now
|
||||||
"""Initialize the thermostat over switch."""
|
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||||
super().__init__(hass, unique_id, name, entry_infos)
|
# """Initialize the thermostat over switch."""
|
||||||
|
# super().__init__(hass, unique_id, name, entry_infos)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_over_climate(self) -> bool:
|
def is_over_climate(self) -> bool:
|
||||||
@@ -62,6 +63,183 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
else:
|
else:
|
||||||
return super.hvac_modes
|
return super.hvac_modes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mean_cycle_power(self) -> float | None:
|
||||||
|
"""Returns the mean power consumption during the cycle"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_mode(self) -> str | None:
|
||||||
|
"""Return the fan setting.
|
||||||
|
|
||||||
|
Requires ClimateEntityFeature.FAN_MODE.
|
||||||
|
"""
|
||||||
|
if self.underlying_entity(0):
|
||||||
|
return self.underlying_entity(0).fan_mode
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_modes(self) -> list[str] | None:
|
||||||
|
"""Return the list of available fan modes.
|
||||||
|
|
||||||
|
Requires ClimateEntityFeature.FAN_MODE.
|
||||||
|
"""
|
||||||
|
if self.underlying_entity(0):
|
||||||
|
return self.underlying_entity(0).fan_modes
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def swing_mode(self) -> str | None:
|
||||||
|
"""Return the swing setting.
|
||||||
|
|
||||||
|
Requires ClimateEntityFeature.SWING_MODE.
|
||||||
|
"""
|
||||||
|
if self.underlying_entity(0):
|
||||||
|
return self.underlying_entity(0).swing_mode
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def swing_modes(self) -> list[str] | None:
|
||||||
|
"""Return the list of available swing modes.
|
||||||
|
|
||||||
|
Requires ClimateEntityFeature.SWING_MODE.
|
||||||
|
"""
|
||||||
|
if self.underlying_entity(0):
|
||||||
|
return self.underlying_entity(0).swing_modes
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature_unit(self) -> str:
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
if self.underlying_entity(0):
|
||||||
|
return self.underlying_entity(0).temperature_unit
|
||||||
|
|
||||||
|
return self._unit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Return the list of supported features."""
|
||||||
|
if self.underlying_entity(0):
|
||||||
|
return self.underlying_entity(0).supported_features | self._support_flags
|
||||||
|
|
||||||
|
return self._support_flags
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature_step(self) -> float | None:
|
||||||
|
"""Return the supported step of target temperature."""
|
||||||
|
if self.underlying_entity(0):
|
||||||
|
return self.underlying_entity(0).target_temperature_step
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature_high(self) -> float | None:
|
||||||
|
"""Return the highbound target temperature we try to reach.
|
||||||
|
|
||||||
|
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||||
|
"""
|
||||||
|
if self.underlying_entity(0):
|
||||||
|
return self.underlying_entity(0).target_temperature_high
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature_low(self) -> float | None:
|
||||||
|
"""Return the lowbound target temperature we try to reach.
|
||||||
|
|
||||||
|
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||||
|
"""
|
||||||
|
if self.underlying_entity(0):
|
||||||
|
return self.underlying_entity(0).target_temperature_low
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_aux_heat(self) -> bool | None:
|
||||||
|
"""Return true if aux heater.
|
||||||
|
|
||||||
|
Requires ClimateEntityFeature.AUX_HEAT.
|
||||||
|
"""
|
||||||
|
if self.underlying_entity(0):
|
||||||
|
return self.underlying_entity(0).is_aux_heat
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def turn_aux_heat_on(self) -> None:
|
||||||
|
"""Turn auxiliary heater on."""
|
||||||
|
if self.underlying_entity(0):
|
||||||
|
return self.underlying_entity(0).turn_aux_heat_on()
|
||||||
|
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
async def async_turn_aux_heat_on(self) -> None:
|
||||||
|
"""Turn auxiliary heater on."""
|
||||||
|
for under in self._underlyings:
|
||||||
|
await under.async_turn_aux_heat_on()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def turn_aux_heat_off(self) -> None:
|
||||||
|
"""Turn auxiliary heater off."""
|
||||||
|
for under in self._underlyings:
|
||||||
|
return under.turn_aux_heat_off()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
async def async_turn_aux_heat_off(self) -> None:
|
||||||
|
"""Turn auxiliary heater off."""
|
||||||
|
for under in self._underlyings:
|
||||||
|
await under.async_turn_aux_heat_off()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
async def async_set_fan_mode(self, fan_mode):
|
||||||
|
"""Set new target fan mode."""
|
||||||
|
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
|
||||||
|
if fan_mode is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
for under in self._underlyings:
|
||||||
|
await under.set_fan_mode(fan_mode)
|
||||||
|
self._fan_mode = fan_mode
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
async def async_set_humidity(self, humidity: int):
|
||||||
|
"""Set new target humidity."""
|
||||||
|
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
|
||||||
|
if humidity is None:
|
||||||
|
return
|
||||||
|
for under in self._underlyings:
|
||||||
|
await under.set_humidity(humidity)
|
||||||
|
self._humidity = humidity
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
async def async_set_swing_mode(self, swing_mode):
|
||||||
|
"""Set new target swing operation."""
|
||||||
|
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
|
||||||
|
if swing_mode is None:
|
||||||
|
return
|
||||||
|
for under in self._underlyings:
|
||||||
|
await under.set_swing_mode(swing_mode)
|
||||||
|
self._swing_mode = swing_mode
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
async def _async_internal_set_temperature(self, temperature):
|
||||||
|
"""Set the target temperature and the target temperature of underlying climate if any"""
|
||||||
|
await super()._async_internal_set_temperature(temperature)
|
||||||
|
|
||||||
|
for under in self._underlyings:
|
||||||
|
await under.set_temperature(
|
||||||
|
temperature, self._attr_max_temp, self._attr_min_temp
|
||||||
|
)
|
||||||
|
|
||||||
|
@overrides
|
||||||
def post_init(self, entry_infos):
|
def post_init(self, entry_infos):
|
||||||
""" Initialize the Thermostat"""
|
""" Initialize the Thermostat"""
|
||||||
|
|
||||||
@@ -81,6 +259,7 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@overrides
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Run when entity about to be added."""
|
"""Run when entity about to be added."""
|
||||||
_LOGGER.debug("Calling async_added_to_hass")
|
_LOGGER.debug("Calling async_added_to_hass")
|
||||||
@@ -105,6 +284,7 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@overrides
|
||||||
def update_custom_attributes(self):
|
def update_custom_attributes(self):
|
||||||
""" Custom attributes """
|
""" Custom attributes """
|
||||||
super().update_custom_attributes()
|
super().update_custom_attributes()
|
||||||
@@ -131,6 +311,7 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
self._attr_extra_state_attributes,
|
self._attr_extra_state_attributes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@overrides
|
||||||
def recalculate(self):
|
def recalculate(self):
|
||||||
"""A utility function to force the calculation of a the algo and
|
"""A utility function to force the calculation of a the algo and
|
||||||
update the custom attributes and write the state
|
update the custom attributes and write the state
|
||||||
@@ -138,3 +319,207 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
_LOGGER.debug("%s - recalculate all", self)
|
_LOGGER.debug("%s - recalculate all", self)
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
async def restore_hvac_mode(self, need_control_heating=False):
|
||||||
|
"""Restore a previous hvac_mod"""
|
||||||
|
old_hvac_mode = self.hvac_mode
|
||||||
|
|
||||||
|
await super().restore_hvac_mode(need_control_heating=need_control_heating)
|
||||||
|
|
||||||
|
# Issue 133 - force the temperature in over_climate mode if unerlying are turned on
|
||||||
|
if old_hvac_mode == HVACMode.OFF and self.hvac_mode != HVACMode.OFF:
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - Force resent target temp cause we turn on some over climate"
|
||||||
|
)
|
||||||
|
await self._async_internal_set_temperature(self._target_temp)
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def incremente_energy(self):
|
||||||
|
"""increment the energy counter if device is active"""
|
||||||
|
|
||||||
|
if self.hvac_mode == HVACMode.OFF:
|
||||||
|
return
|
||||||
|
|
||||||
|
added_energy = 0
|
||||||
|
if self.is_over_climate and self._underlying_climate_delta_t is not None:
|
||||||
|
added_energy = self._device_power * self._underlying_climate_delta_t
|
||||||
|
|
||||||
|
self._total_energy += added_energy
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s - added energy is %.3f . Total energy is now: %.3f",
|
||||||
|
self,
|
||||||
|
added_energy,
|
||||||
|
self._total_energy,
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
async def _async_climate_changed(self, event):
|
||||||
|
"""Handle unerdlying climate state changes.
|
||||||
|
This method takes the underlying values and update the VTherm with them.
|
||||||
|
To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received
|
||||||
|
less than 10 sec after the last command. What we want here is to take the values
|
||||||
|
from underlyings ONLY if someone have change directly on the underlying and not
|
||||||
|
as a return of the command. The only thing we take all the time is the HVACAction
|
||||||
|
which is important for feedaback and which cannot generates loops.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def end_climate_changed(changes):
|
||||||
|
"""To end the event management"""
|
||||||
|
if changes:
|
||||||
|
self.async_write_ha_state()
|
||||||
|
self.update_custom_attributes()
|
||||||
|
await self.async_control_heating()
|
||||||
|
|
||||||
|
new_state = event.data.get("new_state")
|
||||||
|
_LOGGER.debug("%s - _async_climate_changed new_state is %s", self, new_state)
|
||||||
|
if not new_state:
|
||||||
|
return
|
||||||
|
|
||||||
|
changes = False
|
||||||
|
new_hvac_mode = new_state.state
|
||||||
|
|
||||||
|
old_state = event.data.get("old_state")
|
||||||
|
old_hvac_action = (
|
||||||
|
old_state.attributes.get("hvac_action")
|
||||||
|
if old_state and old_state.attributes
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
new_hvac_action = (
|
||||||
|
new_state.attributes.get("hvac_action")
|
||||||
|
if new_state and new_state.attributes
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
old_state_date_changed = (
|
||||||
|
old_state.last_changed if old_state and old_state.last_changed else None
|
||||||
|
)
|
||||||
|
old_state_date_updated = (
|
||||||
|
old_state.last_updated if old_state and old_state.last_updated else None
|
||||||
|
)
|
||||||
|
new_state_date_changed = (
|
||||||
|
new_state.last_changed if new_state and new_state.last_changed else None
|
||||||
|
)
|
||||||
|
new_state_date_updated = (
|
||||||
|
new_state.last_updated if new_state and new_state.last_updated else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
|
||||||
|
# Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is
|
||||||
|
# if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE:
|
||||||
|
# _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
|
||||||
|
# new_hvac_mode = HVACMode.OFF
|
||||||
|
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - Underlying climate changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
|
||||||
|
self,
|
||||||
|
new_hvac_mode,
|
||||||
|
self._hvac_mode,
|
||||||
|
new_hvac_action,
|
||||||
|
old_hvac_action,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s",
|
||||||
|
self,
|
||||||
|
self._last_change_time,
|
||||||
|
old_state_date_changed,
|
||||||
|
old_state_date_updated,
|
||||||
|
new_state_date_changed,
|
||||||
|
new_state_date_updated,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Interpretation of hvac action
|
||||||
|
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||||
|
HVACAction.COOLING,
|
||||||
|
HVACAction.DRYING,
|
||||||
|
HVACAction.FAN,
|
||||||
|
HVACAction.HEATING,
|
||||||
|
]
|
||||||
|
if old_hvac_action not in HVAC_ACTION_ON and new_hvac_action in HVAC_ACTION_ON:
|
||||||
|
self._underlying_climate_start_hvac_action_date = (
|
||||||
|
self.get_last_updated_date_or_now(new_state)
|
||||||
|
)
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - underlying just switch ON. Set power and energy start date %s",
|
||||||
|
self,
|
||||||
|
self._underlying_climate_start_hvac_action_date.isoformat(),
|
||||||
|
)
|
||||||
|
changes = True
|
||||||
|
|
||||||
|
if old_hvac_action in HVAC_ACTION_ON and new_hvac_action not in HVAC_ACTION_ON:
|
||||||
|
stop_power_date = self.get_last_updated_date_or_now(new_state)
|
||||||
|
if self._underlying_climate_start_hvac_action_date:
|
||||||
|
delta = (
|
||||||
|
stop_power_date - self._underlying_climate_start_hvac_action_date
|
||||||
|
)
|
||||||
|
self._underlying_climate_delta_t = delta.total_seconds() / 3600.0
|
||||||
|
|
||||||
|
# increment energy at the end of the cycle
|
||||||
|
self.incremente_energy()
|
||||||
|
|
||||||
|
self._underlying_climate_start_hvac_action_date = None
|
||||||
|
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - underlying just switch OFF at %s. delta_h=%.3f h",
|
||||||
|
self,
|
||||||
|
stop_power_date.isoformat(),
|
||||||
|
self._underlying_climate_delta_t,
|
||||||
|
)
|
||||||
|
changes = True
|
||||||
|
|
||||||
|
# Issue #120 - Some TRV are chaning target temperature a very long time (6 sec) after the change.
|
||||||
|
# In that case a loop is possible if a user change multiple times during this 6 sec.
|
||||||
|
if new_state_date_updated and self._last_change_time:
|
||||||
|
delta = (new_state_date_updated - self._last_change_time).total_seconds()
|
||||||
|
if delta < 10:
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - underlying event is received less than 10 sec after command. Forget it to avoid loop",
|
||||||
|
self,
|
||||||
|
)
|
||||||
|
await end_climate_changed(changes)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
new_hvac_mode
|
||||||
|
in [
|
||||||
|
HVACMode.OFF,
|
||||||
|
HVACMode.HEAT,
|
||||||
|
HVACMode.COOL,
|
||||||
|
HVACMode.HEAT_COOL,
|
||||||
|
HVACMode.DRY,
|
||||||
|
HVACMode.AUTO,
|
||||||
|
HVACMode.FAN_ONLY,
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
and self._hvac_mode != new_hvac_mode
|
||||||
|
):
|
||||||
|
changes = True
|
||||||
|
self._hvac_mode = new_hvac_mode
|
||||||
|
# Update all underlyings state
|
||||||
|
if self.is_over_climate:
|
||||||
|
for under in self._underlyings:
|
||||||
|
await under.set_hvac_mode(new_hvac_mode)
|
||||||
|
|
||||||
|
if not changes:
|
||||||
|
# try to manage new target temperature set if state
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Do temperature check. temperature is %s, new_state.attributes is %s",
|
||||||
|
self.target_temperature,
|
||||||
|
new_state.attributes,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
self.is_over_climate
|
||||||
|
and new_state.attributes
|
||||||
|
and (new_target_temp := new_state.attributes.get("temperature"))
|
||||||
|
and new_target_temp != self.target_temperature
|
||||||
|
):
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - Target temp in underlying have change to %s",
|
||||||
|
self,
|
||||||
|
new_target_temp,
|
||||||
|
)
|
||||||
|
await self.async_set_temperature(temperature=new_target_temp)
|
||||||
|
changes = True
|
||||||
|
|
||||||
|
await end_climate_changed(changes)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
""" A climate over switch classe """
|
""" A climate over switch classe """
|
||||||
import logging
|
import logging
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.event import async_track_state_change_event
|
from homeassistant.helpers.event import async_track_state_change_event
|
||||||
from homeassistant.components.climate import HVACMode
|
from homeassistant.components.climate import HVACMode
|
||||||
|
|
||||||
@@ -10,12 +10,13 @@ from .const import (
|
|||||||
CONF_HEATER,
|
CONF_HEATER,
|
||||||
CONF_HEATER_2,
|
CONF_HEATER_2,
|
||||||
CONF_HEATER_3,
|
CONF_HEATER_3,
|
||||||
CONF_HEATER_4
|
CONF_HEATER_4,
|
||||||
|
overrides
|
||||||
)
|
)
|
||||||
|
|
||||||
from .base_thermostat import BaseThermostat
|
from .base_thermostat import BaseThermostat
|
||||||
|
|
||||||
from .underlyings import UnderlyingSwitch
|
from .underlyings import UnderlyingSwitch
|
||||||
|
from .prop_algorithm import PropAlgorithm
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -29,19 +30,30 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
"cycle_min", "function", "tpi_coef_int", "tpi_coef_ext"
|
"cycle_min", "function", "tpi_coef_int", "tpi_coef_ext"
|
||||||
}))
|
}))
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
# useless for now
|
||||||
"""Initialize the thermostat over switch."""
|
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||||
super().__init__(hass, unique_id, name, entry_infos)
|
# """Initialize the thermostat over switch."""
|
||||||
|
# super().__init__(hass, unique_id, name, entry_infos)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_over_switch(self) -> bool:
|
def is_over_switch(self) -> bool:
|
||||||
""" True if the Thermostat is over_switch"""
|
""" True if the Thermostat is over_switch"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@overrides
|
||||||
def post_init(self, entry_infos):
|
def post_init(self, entry_infos):
|
||||||
""" Initialize the Thermostat"""
|
""" Initialize the Thermostat"""
|
||||||
|
|
||||||
super().post_init(entry_infos)
|
super().post_init(entry_infos)
|
||||||
|
|
||||||
|
self._prop_algorithm = PropAlgorithm(
|
||||||
|
self._proportional_function,
|
||||||
|
self._tpi_coef_int,
|
||||||
|
self._tpi_coef_ext,
|
||||||
|
self._cycle_min,
|
||||||
|
self._minimal_activation_delay,
|
||||||
|
)
|
||||||
|
|
||||||
lst_switches = [entry_infos.get(CONF_HEATER)]
|
lst_switches = [entry_infos.get(CONF_HEATER)]
|
||||||
if entry_infos.get(CONF_HEATER_2):
|
if entry_infos.get(CONF_HEATER_2):
|
||||||
lst_switches.append(entry_infos.get(CONF_HEATER_2))
|
lst_switches.append(entry_infos.get(CONF_HEATER_2))
|
||||||
@@ -61,6 +73,9 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._should_relaunch_control_heating = False
|
||||||
|
|
||||||
|
@overrides
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Run when entity about to be added."""
|
"""Run when entity about to be added."""
|
||||||
_LOGGER.debug("Calling async_added_to_hass")
|
_LOGGER.debug("Calling async_added_to_hass")
|
||||||
@@ -77,6 +92,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
|
|
||||||
self.hass.create_task(self.async_control_heating())
|
self.hass.create_task(self.async_control_heating())
|
||||||
|
|
||||||
|
@overrides
|
||||||
def update_custom_attributes(self):
|
def update_custom_attributes(self):
|
||||||
""" Custom attributes """
|
""" Custom attributes """
|
||||||
super().update_custom_attributes()
|
super().update_custom_attributes()
|
||||||
@@ -115,6 +131,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
self._attr_extra_state_attributes,
|
self._attr_extra_state_attributes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@overrides
|
||||||
def recalculate(self):
|
def recalculate(self):
|
||||||
"""A utility function to force the calculation of a the algo and
|
"""A utility function to force the calculation of a the algo and
|
||||||
update the custom attributes and write the state
|
update the custom attributes and write the state
|
||||||
@@ -128,3 +145,32 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
)
|
)
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def incremente_energy(self):
|
||||||
|
"""increment the energy counter if device is active"""
|
||||||
|
if self.hvac_mode == HVACMode.OFF:
|
||||||
|
return
|
||||||
|
|
||||||
|
added_energy = 0
|
||||||
|
if not self.is_over_climate and self.mean_cycle_power is not None:
|
||||||
|
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
|
||||||
|
|
||||||
|
self._total_energy += added_energy
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s - added energy is %.3f . Total energy is now: %.3f",
|
||||||
|
self,
|
||||||
|
added_energy,
|
||||||
|
self._total_energy,
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_switch_changed(self, event):
|
||||||
|
"""Handle heater switch state changes."""
|
||||||
|
new_state = event.data.get("new_state")
|
||||||
|
old_state = event.data.get("old_state")
|
||||||
|
if new_state is None:
|
||||||
|
return
|
||||||
|
if old_state is None:
|
||||||
|
self.hass.create_task(self._check_initial_state())
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.climate import HVACMode, HVACAction
|
from homeassistant.components.climate import HVACMode
|
||||||
|
|
||||||
from .base_thermostat import BaseThermostat
|
from .base_thermostat import BaseThermostat
|
||||||
|
from .prop_algorithm import PropAlgorithm
|
||||||
|
|
||||||
from .const import CONF_VALVE, CONF_VALVE_2, CONF_VALVE_3, CONF_VALVE_4
|
from .const import CONF_VALVE, CONF_VALVE_2, CONF_VALVE_3, CONF_VALVE_4, overrides
|
||||||
|
|
||||||
from .underlyings import UnderlyingValve
|
from .underlyings import UnderlyingValve
|
||||||
|
|
||||||
@@ -26,9 +26,10 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
"cycle_min", "function", "tpi_coef_int", "tpi_coef_ext"
|
"cycle_min", "function", "tpi_coef_int", "tpi_coef_ext"
|
||||||
}))
|
}))
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
# Useless for now
|
||||||
"""Initialize the thermostat over switch."""
|
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||||
super().__init__(hass, unique_id, name, entry_infos)
|
# """Initialize the thermostat over switch."""
|
||||||
|
# super().__init__(hass, unique_id, name, entry_infos)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_over_valve(self) -> bool:
|
def is_over_valve(self) -> bool:
|
||||||
@@ -43,10 +44,19 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
else:
|
else:
|
||||||
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
|
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
|
||||||
|
|
||||||
|
@overrides
|
||||||
def post_init(self, entry_infos):
|
def post_init(self, entry_infos):
|
||||||
""" Initialize the Thermostat"""
|
""" Initialize the Thermostat"""
|
||||||
|
|
||||||
super().post_init(entry_infos)
|
super().post_init(entry_infos)
|
||||||
|
self._prop_algorithm = PropAlgorithm(
|
||||||
|
self._proportional_function,
|
||||||
|
self._tpi_coef_int,
|
||||||
|
self._tpi_coef_ext,
|
||||||
|
self._cycle_min,
|
||||||
|
self._minimal_activation_delay,
|
||||||
|
)
|
||||||
|
|
||||||
lst_valves = [entry_infos.get(CONF_VALVE)]
|
lst_valves = [entry_infos.get(CONF_VALVE)]
|
||||||
if entry_infos.get(CONF_VALVE_2):
|
if entry_infos.get(CONF_VALVE_2):
|
||||||
lst_valves.append(entry_infos.get(CONF_VALVE_2))
|
lst_valves.append(entry_infos.get(CONF_VALVE_2))
|
||||||
@@ -64,6 +74,9 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._should_relaunch_control_heating = False
|
||||||
|
|
||||||
|
@overrides
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Run when entity about to be added."""
|
"""Run when entity about to be added."""
|
||||||
_LOGGER.debug("Calling async_added_to_hass")
|
_LOGGER.debug("Calling async_added_to_hass")
|
||||||
@@ -96,6 +109,7 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
new_state = event.data.get("new_state")
|
new_state = event.data.get("new_state")
|
||||||
_LOGGER.debug("%s - _async_valve_changed new_state is %s", self, new_state.state)
|
_LOGGER.debug("%s - _async_valve_changed new_state is %s", self, new_state.state)
|
||||||
|
|
||||||
|
@overrides
|
||||||
def update_custom_attributes(self):
|
def update_custom_attributes(self):
|
||||||
""" Custom attributes """
|
""" Custom attributes """
|
||||||
super().update_custom_attributes()
|
super().update_custom_attributes()
|
||||||
@@ -134,6 +148,7 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
self._attr_extra_state_attributes,
|
self._attr_extra_state_attributes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@overrides
|
||||||
def recalculate(self):
|
def recalculate(self):
|
||||||
"""A utility function to force the calculation of a the algo and
|
"""A utility function to force the calculation of a the algo and
|
||||||
update the custom attributes and write the state
|
update the custom attributes and write the state
|
||||||
@@ -153,3 +168,21 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
|
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def incremente_energy(self):
|
||||||
|
"""increment the energy counter if device is active"""
|
||||||
|
if self.hvac_mode == HVACMode.OFF:
|
||||||
|
return
|
||||||
|
|
||||||
|
added_energy = 0
|
||||||
|
if not self.is_over_climate and self.mean_cycle_power is not None:
|
||||||
|
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
|
||||||
|
|
||||||
|
self._total_energy += added_energy
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s - added energy is %.3f . Total energy is now: %.3f",
|
||||||
|
self,
|
||||||
|
added_energy,
|
||||||
|
self._total_energy,
|
||||||
|
)
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
|
# pylint: disable=protected-access, unused-argument, line-too-long
|
||||||
""" Test the Power management """
|
""" Test the Power management """
|
||||||
from unittest.mock import patch, call
|
from unittest.mock import patch, call
|
||||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from homeassistant.const import UnitOfTemperature
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
|
||||||
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
@@ -50,7 +49,7 @@ async def test_power_management_hvac_off(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity: VersatileThermostat = await create_thermostat(
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
hass, entry, "climate.theoverswitchmockname"
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
@@ -136,7 +135,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity: VersatileThermostat = await create_thermostat(
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
hass, entry, "climate.theoverswitchmockname"
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
@@ -270,7 +269,7 @@ async def test_power_management_energy_over_switch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity: VersatileThermostat = await create_thermostat(
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
hass, entry, "climate.theoverswitchmockname"
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
@@ -305,9 +304,9 @@ async def test_power_management_energy_over_switch(
|
|||||||
assert mock_heater_off.call_count == 0
|
assert mock_heater_off.call_count == 0
|
||||||
|
|
||||||
entity.incremente_energy()
|
entity.incremente_energy()
|
||||||
assert entity.total_energy == 100 * 5 / 60.0
|
assert entity.total_energy == round(100 * 5 / 60.0, 2)
|
||||||
entity.incremente_energy()
|
entity.incremente_energy()
|
||||||
assert entity.total_energy == 2 * 100 * 5 / 60.0
|
assert entity.total_energy == round(2 * 100 * 5 / 60.0, 2)
|
||||||
|
|
||||||
# change temperature to a higher value
|
# change temperature to a higher value
|
||||||
with patch(
|
with patch(
|
||||||
@@ -398,7 +397,7 @@ async def test_power_management_energy_over_climate(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity: VersatileThermostat = await create_thermostat(
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
hass, entry, "climate.theoverclimatemockname"
|
hass, entry, "climate.theoverclimatemockname"
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ async def test_sensors_over_switch(
|
|||||||
entity.incremente_energy()
|
entity.incremente_energy()
|
||||||
|
|
||||||
await energy_sensor.async_my_climate_changed()
|
await energy_sensor.async_my_climate_changed()
|
||||||
assert energy_sensor.state == 16.667
|
assert energy_sensor.state == round(16.667, 2)
|
||||||
assert energy_sensor.device_class == SensorDeviceClass.ENERGY
|
assert energy_sensor.device_class == SensorDeviceClass.ENERGY
|
||||||
assert energy_sensor.state_class == SensorStateClass.TOTAL_INCREASING
|
assert energy_sensor.state_class == SensorStateClass.TOTAL_INCREASING
|
||||||
# because device_power is 200
|
# because device_power is 200
|
||||||
|
|||||||
Reference in New Issue
Block a user