Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e63213d22a | |||
| fb7ee1bdac | |||
| ca86b310c4 | |||
| 23074e6f46 | |||
| 718315c4fe | |||
| 46278ca9a3 | |||
| 0b81a94d0f | |||
| 33590886c1 | |||
| 039b372a53 | |||
| a161540f10 | |||
| 8bbcafdf4a |
@@ -118,6 +118,37 @@ template:
|
|||||||
unique_id: maison_occupee
|
unique_id: maison_occupee
|
||||||
state: "{{is_state('person.jmc', 'home') }}"
|
state: "{{is_state('person.jmc', 'home') }}"
|
||||||
device_class: occupancy
|
device_class: occupancy
|
||||||
|
- sensor:
|
||||||
|
- name: "Total énergie switch1"
|
||||||
|
unique_id: total_energie_switch1
|
||||||
|
unit_of_measurement: "kWh"
|
||||||
|
device_class: energy
|
||||||
|
state_class: total_increasing
|
||||||
|
state: >
|
||||||
|
{% set energy = state_attr('climate.thermostat_switch_1', 'total_energy') %}
|
||||||
|
{% if energy == 'unavailable' or energy is none%}unavailable{% else %}
|
||||||
|
{{ ((energy | float) / 1.0) | round(2, default=0) }}
|
||||||
|
{% endif %}
|
||||||
|
- name: "Total énergie climate 2"
|
||||||
|
unique_id: total_energie_climate2
|
||||||
|
unit_of_measurement: "kWh"
|
||||||
|
device_class: energy
|
||||||
|
state_class: total_increasing
|
||||||
|
state: >
|
||||||
|
{% set energy = state_attr('climate.thermostat_climate_2', 'total_energy') %}
|
||||||
|
{% if energy == 'unavailable' or energy is none%}unavailable{% else %}
|
||||||
|
{{ ((energy | float) / 1.0) | round(2, default=0) }}
|
||||||
|
{% endif %}
|
||||||
|
- name: "Total énergie chambre"
|
||||||
|
unique_id: total_energie_chambre
|
||||||
|
unit_of_measurement: "kWh"
|
||||||
|
device_class: energy
|
||||||
|
state_class: total_increasing
|
||||||
|
state: >
|
||||||
|
{% set energy = state_attr('climate.thermostat_chambre', 'total_energy') %}
|
||||||
|
{% if energy == 'unavailable' or energy is none%}unavailable{% else %}
|
||||||
|
{{ ((energy | float) / 1.0) | round(2, default=0) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
switch:
|
switch:
|
||||||
- platform: template
|
- platform: template
|
||||||
|
|||||||
+2
-2
@@ -323,8 +323,8 @@ Le pourcentage est calculé avec cette formule :
|
|||||||
Les valeurs par défaut pour coef_int et coef_ext sont respectivement : ``0.6`` et ``0.01``. Ces valeurs par défaut conviennent à une pièce standard bien isolée.
|
Les valeurs par défaut pour coef_int et coef_ext sont respectivement : ``0.6`` et ``0.01``. Ces valeurs par défaut conviennent à une pièce standard bien isolée.
|
||||||
|
|
||||||
Pour régler ces coefficients, gardez à l'esprit que :
|
Pour régler ces coefficients, gardez à l'esprit que :
|
||||||
1. **si la température cible n'est pas atteinte** après une situation stable, vous devez augmenter le ``coef_ext`` (le ``on_percent`` est trop élevé),
|
1. **si la température cible n'est pas atteinte** après une situation stable, vous devez augmenter le ``coef_ext`` (le ``on_percent`` est trop bas),
|
||||||
2. **si la température cible est dépassée** après une situation stable, vous devez diminuer le ``coef_ext`` (le ``on_percent`` est trop bas),
|
2. **si la température cible est dépassée** après une situation stable, vous devez diminuer le ``coef_ext`` (le ``on_percent`` est trop haut),
|
||||||
3. **si l'atteinte de la température cible est trop lente**, vous pouvez augmenter le ``coef_int`` pour donner plus de puissance au réchauffeur,
|
3. **si l'atteinte de la température cible est trop lente**, vous pouvez augmenter le ``coef_int`` pour donner plus de puissance au réchauffeur,
|
||||||
4. **si l'atteinte de la température cible est trop rapide et que des oscillations apparaissent** autour de la cible, vous pouvez diminuer le ``coef_int`` pour donner moins de puissance au radiateur
|
4. **si l'atteinte de la température cible est trop rapide et que des oscillations apparaissent** autour de la cible, vous pouvez diminuer le ``coef_int`` pour donner moins de puissance au radiateur
|
||||||
|
|
||||||
|
|||||||
@@ -309,8 +309,8 @@ The percentage is calculated with this formula:
|
|||||||
Defaults values for coef_int and coef_ext are respectively: ``0.6`` and ``0.01``. Those defaults values are suitable for a standard well isolated room.
|
Defaults values for coef_int and coef_ext are respectively: ``0.6`` and ``0.01``. Those defaults values are suitable for a standard well isolated room.
|
||||||
|
|
||||||
To tune those coefficients keep in mind that:
|
To tune those coefficients keep in mind that:
|
||||||
1. **if target temperature is not reach** after stable situation, you have to augment the ``coef_ext`` (the ``on_percent`` is too high),
|
1. **if target temperature is not reach** after stable situation, you have to augment the ``coef_ext`` (the ``on_percent`` is too low),
|
||||||
2. **if target temperature is exceeded** after stable situation, you have to decrease the ``coef_ext`` (the ``on_percent`` is too low),
|
2. **if target temperature is exceeded** after stable situation, you have to decrease the ``coef_ext`` (the ``on_percent`` is too high),
|
||||||
3. **if reaching the target temperature is too slow**, you can increase the ``coef_int`` to give more power to the heater,
|
3. **if reaching the target temperature is too slow**, you can increase the ``coef_int`` to give more power to the heater,
|
||||||
4. **if reaching the target temperature is too fast and some oscillations appears** around the target, you can decrease the ``coef_int`` to give less power to the heater
|
4. **if reaching the target temperature is too fast and some oscillations appears** around the target, you can decrease the ``coef_int`` to give less power to the heater
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from datetime import timedelta, datetime
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
callback,
|
callback,
|
||||||
@@ -134,6 +135,8 @@ from .const import (
|
|||||||
CONF_CLIMATE,
|
CONF_CLIMATE,
|
||||||
UnknownEntity,
|
UnknownEntity,
|
||||||
EventType,
|
EventType,
|
||||||
|
ATTR_MEAN_POWER_CYCLE,
|
||||||
|
ATTR_TOTAL_ENERGY,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .prop_algorithm import PropAlgorithm
|
from .prop_algorithm import PropAlgorithm
|
||||||
@@ -201,6 +204,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# _registry: dict[str, object] = {}
|
# _registry: dict[str, object] = {}
|
||||||
_last_temperature_mesure: datetime
|
_last_temperature_mesure: datetime
|
||||||
_last_ext_temperature_mesure: datetime
|
_last_ext_temperature_mesure: datetime
|
||||||
|
_total_energy: float
|
||||||
|
_overpowering_state: bool
|
||||||
|
_window_state: bool
|
||||||
|
_motion_state: bool
|
||||||
|
_presence_state: bool
|
||||||
|
_security_state: bool
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
@@ -251,6 +260,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
self._attr_translation_key = "versatile_thermostat"
|
self._attr_translation_key = "versatile_thermostat"
|
||||||
|
|
||||||
|
self._total_energy = None
|
||||||
|
self._underlying_climate_start_hvac_action_date = None
|
||||||
|
self._underlying_climate_delta_t = 0
|
||||||
|
|
||||||
|
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
|
||||||
|
|
||||||
self.post_init(entry_infos)
|
self.post_init(entry_infos)
|
||||||
|
|
||||||
def post_init(self, entry_infos):
|
def post_init(self, entry_infos):
|
||||||
@@ -361,8 +376,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
and self._device_power
|
and self._device_power
|
||||||
):
|
):
|
||||||
self._pmax_on = True
|
self._pmax_on = True
|
||||||
self._current_power = 0
|
|
||||||
self._current_power_max = 0
|
|
||||||
else:
|
else:
|
||||||
_LOGGER.info("%s - Power management is not fully configured", self)
|
_LOGGER.info("%s - Power management is not fully configured", self)
|
||||||
|
|
||||||
@@ -396,8 +409,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
or DEFAULT_SECURITY_DEFAULT_ON_PERCENT
|
or DEFAULT_SECURITY_DEFAULT_ON_PERCENT
|
||||||
)
|
)
|
||||||
self._minimal_activation_delay = entry_infos.get(CONF_MINIMAL_ACTIVATION_DELAY)
|
self._minimal_activation_delay = entry_infos.get(CONF_MINIMAL_ACTIVATION_DELAY)
|
||||||
self._last_temperature_mesure = datetime.now()
|
self._last_temperature_mesure = datetime.now(tz=self._current_tz)
|
||||||
self._last_ext_temperature_mesure = datetime.now()
|
self._last_ext_temperature_mesure = datetime.now(tz=self._current_tz)
|
||||||
self._security_state = False
|
self._security_state = False
|
||||||
self._saved_hvac_mode = None
|
self._saved_hvac_mode = None
|
||||||
|
|
||||||
@@ -441,6 +454,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
if self._motion_on:
|
if self._motion_on:
|
||||||
self._attr_preset_modes.append(PRESET_ACTIVITY)
|
self._attr_preset_modes.append(PRESET_ACTIVITY)
|
||||||
|
|
||||||
|
self._total_energy = 0
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s - Creation of a new VersatileThermostat entity: unique_id=%s heater_entity_id=%s",
|
"%s - Creation of a new VersatileThermostat entity: unique_id=%s heater_entity_id=%s",
|
||||||
self,
|
self,
|
||||||
@@ -780,6 +795,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
else:
|
else:
|
||||||
self._hvac_mode = HVACMode.OFF
|
self._hvac_mode = HVACMode.OFF
|
||||||
|
|
||||||
|
old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY)
|
||||||
|
if old_total_energy:
|
||||||
|
self._total_energy = old_total_energy
|
||||||
else:
|
else:
|
||||||
# No previous state, try and restore defaults
|
# No previous state, try and restore defaults
|
||||||
if self._target_temp is None:
|
if self._target_temp is None:
|
||||||
@@ -981,6 +999,34 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mean_cycle_power(self) -> float | None:
|
||||||
|
"""Returns tne mean power consumption during the cycle"""
|
||||||
|
if not self._device_power or self._is_over_climate:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return float(self._device_power * self._prop_algorithm.on_percent)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_energy(self) -> float | None:
|
||||||
|
"""Returns the total energy calculated for this thermostast"""
|
||||||
|
return self._total_energy
|
||||||
|
|
||||||
|
@property
|
||||||
|
def overpowering_state(self) -> bool | None:
|
||||||
|
"""Get the overpowering_state"""
|
||||||
|
return self._overpowering_state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def window_state(self) -> bool | None:
|
||||||
|
"""Get the window_state"""
|
||||||
|
return self._window_state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def motion_state(self) -> bool | None:
|
||||||
|
"""Get the motion_state"""
|
||||||
|
return self._motion_state
|
||||||
|
|
||||||
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_climate:
|
if self._is_over_climate and self._underlying_climate:
|
||||||
@@ -1120,7 +1166,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
):
|
):
|
||||||
self._last_temperature_mesure = (
|
self._last_temperature_mesure = (
|
||||||
self._last_ext_temperature_mesure
|
self._last_ext_temperature_mesure
|
||||||
) = datetime.now()
|
) = datetime.now(tz=self._current_tz)
|
||||||
|
|
||||||
def find_preset_temp(self, preset_mode):
|
def find_preset_temp(self, preset_mode):
|
||||||
"""Find the right temperature of a preset considering the presence if configured"""
|
"""Find the right temperature of a preset considering the presence if configured"""
|
||||||
@@ -1219,6 +1265,22 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, data, context=self._context
|
CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, data, context=self._context
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_state_date_or_now(self, state: State):
|
||||||
|
"""Extract the last_changed state from State or return now if not available"""
|
||||||
|
return (
|
||||||
|
state.last_changed.astimezone(self._current_tz)
|
||||||
|
if state.last_changed is not None
|
||||||
|
else datetime.now(tz=self._current_tz)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_last_updated_date_or_now(self, state: State):
|
||||||
|
"""Extract the last_changed state from State or return now if not available"""
|
||||||
|
return (
|
||||||
|
state.last_updated.astimezone(self._current_tz)
|
||||||
|
if state.last_updated is not None
|
||||||
|
else datetime.now(tz=self._current_tz)
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def entry_update_listener(
|
async def entry_update_listener(
|
||||||
self, _, config_entry: ConfigEntry # hass: HomeAssistant,
|
self, _, config_entry: ConfigEntry # hass: HomeAssistant,
|
||||||
@@ -1269,8 +1331,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._hvac_mode,
|
self._hvac_mode,
|
||||||
self._saved_hvac_mode,
|
self._saved_hvac_mode,
|
||||||
)
|
)
|
||||||
if new_state is None or old_state is None or new_state.state == old_state.state:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check delay condition
|
# Check delay condition
|
||||||
async def try_window_condition(_):
|
async def try_window_condition(_):
|
||||||
@@ -1288,6 +1348,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Window delay condition is not satisfied. Ignore window event"
|
"Window delay condition is not satisfied. Ignore window event"
|
||||||
)
|
)
|
||||||
|
self._window_state = old_state.state
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.debug("%s - Window delay condition is satisfied", self)
|
_LOGGER.debug("%s - Window delay condition is satisfied", self)
|
||||||
@@ -1310,12 +1371,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
|
|
||||||
|
if new_state is None or old_state is None or new_state.state == old_state.state:
|
||||||
|
return try_window_condition
|
||||||
|
|
||||||
if self._window_call_cancel:
|
if self._window_call_cancel:
|
||||||
self._window_call_cancel()
|
self._window_call_cancel()
|
||||||
self._window_call_cancel = None
|
self._window_call_cancel = None
|
||||||
self._window_call_cancel = async_call_later(
|
self._window_call_cancel = async_call_later(
|
||||||
self.hass, timedelta(seconds=self._window_delay_sec), try_window_condition
|
self.hass, timedelta(seconds=self._window_delay_sec), try_window_condition
|
||||||
)
|
)
|
||||||
|
# For testing purpose we need to access the inner function
|
||||||
|
return try_window_condition
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def _async_motion_changed(self, event):
|
async def _async_motion_changed(self, event):
|
||||||
@@ -1401,14 +1467,29 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
async def _async_climate_changed(self, event):
|
async def _async_climate_changed(self, event):
|
||||||
"""Handle unerdlying climate state changes."""
|
"""Handle unerdlying climate state changes."""
|
||||||
new_state = event.data.get("new_state")
|
new_state = event.data.get("new_state")
|
||||||
|
_LOGGER.warning("%s - _async_climate_changed new_state is %s", self, new_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
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"%s - Underlying climate changed. Event.new_state is %s, hvac_mode=%s",
|
"%s - Underlying climate changed. Event.new_state is %s, hvac_mode=%s, hvac_action=%s, old_hvac_action=%s",
|
||||||
self,
|
self,
|
||||||
new_state,
|
new_state,
|
||||||
self._hvac_mode,
|
self._hvac_mode,
|
||||||
|
new_hvac_action,
|
||||||
|
old_hvac_action,
|
||||||
)
|
)
|
||||||
# old_state = event.data.get("old_state")
|
|
||||||
if new_state is None or new_state.state not in [
|
if new_state.state in [
|
||||||
HVACMode.OFF,
|
HVACMode.OFF,
|
||||||
HVACMode.HEAT,
|
HVACMode.HEAT,
|
||||||
HVACMode.COOL,
|
HVACMode.COOL,
|
||||||
@@ -1417,8 +1498,45 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
HVACMode.AUTO,
|
HVACMode.AUTO,
|
||||||
HVACMode.FAN_ONLY,
|
HVACMode.FAN_ONLY,
|
||||||
]:
|
]:
|
||||||
return
|
self._hvac_mode = new_state.state
|
||||||
self._hvac_mode = new_state.state
|
|
||||||
|
# Interpretation of hvac
|
||||||
|
HVAC_ACTION_ON = [
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
await self._async_control_heating(True)
|
await self._async_control_heating(True)
|
||||||
|
|
||||||
@@ -1430,9 +1548,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
if math.isnan(cur_temp) or math.isinf(cur_temp):
|
if math.isnan(cur_temp) or math.isinf(cur_temp):
|
||||||
raise ValueError(f"Sensor has illegal state {state.state}")
|
raise ValueError(f"Sensor has illegal state {state.state}")
|
||||||
self._cur_temp = cur_temp
|
self._cur_temp = cur_temp
|
||||||
self._last_temperature_mesure = (
|
|
||||||
state.last_changed if state.last_changed is not None else datetime.now()
|
self._last_temperature_mesure = self.get_state_date_or_now(state)
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s - After setting _last_temperature_mesure %s , state.last_changed.replace=%s",
|
||||||
|
self,
|
||||||
|
self._last_temperature_mesure,
|
||||||
|
state.last_changed.astimezone(self._current_tz),
|
||||||
)
|
)
|
||||||
|
|
||||||
# try to restart if we were in security mode
|
# try to restart if we were in security mode
|
||||||
if self._security_state:
|
if self._security_state:
|
||||||
await self.check_security()
|
await self.check_security()
|
||||||
@@ -1448,9 +1573,15 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
if math.isnan(cur_ext_temp) or math.isinf(cur_ext_temp):
|
if math.isnan(cur_ext_temp) or math.isinf(cur_ext_temp):
|
||||||
raise ValueError(f"Sensor has illegal state {state.state}")
|
raise ValueError(f"Sensor has illegal state {state.state}")
|
||||||
self._cur_ext_temp = cur_ext_temp
|
self._cur_ext_temp = cur_ext_temp
|
||||||
self._last_ext_temperature_mesure = (
|
self._last_ext_temperature_mesure = self.get_state_date_or_now(state)
|
||||||
state.last_changed if state.last_changed is not None else datetime.now()
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s - After setting _last_ext_temperature_mesure %s , state.last_changed.replace=%s",
|
||||||
|
self,
|
||||||
|
self._last_ext_temperature_mesure,
|
||||||
|
state.last_changed.astimezone(self._current_tz),
|
||||||
)
|
)
|
||||||
|
|
||||||
# try to restart if we were in security mode
|
# try to restart if we were in security mode
|
||||||
if self._security_state:
|
if self._security_state:
|
||||||
await self.check_security()
|
await self.check_security()
|
||||||
@@ -1671,7 +1802,20 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not self._pmax_on:
|
if not self._pmax_on:
|
||||||
return
|
_LOGGER.debug(
|
||||||
|
"%s - power not configured. check_overpowering not available", self
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if (
|
||||||
|
self._current_power is None
|
||||||
|
or self._device_power is None
|
||||||
|
or self._current_power_max is None
|
||||||
|
):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"%s - power not valued. check_overpowering not available", self
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
|
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
|
||||||
@@ -1680,6 +1824,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._current_power_max,
|
self._current_power_max,
|
||||||
self._device_power,
|
self._device_power,
|
||||||
)
|
)
|
||||||
|
|
||||||
ret = self._current_power + self._device_power >= self._current_power_max
|
ret = self._current_power + self._device_power >= self._current_power_max
|
||||||
if not self._overpowering_state and ret and not self._hvac_mode == HVACMode.OFF:
|
if not self._overpowering_state and ret and not self._hvac_mode == HVACMode.OFF:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
@@ -1730,12 +1875,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
async def check_security(self) -> bool:
|
async def check_security(self) -> bool:
|
||||||
"""Check if last temperature date is too long"""
|
"""Check if last temperature date is too long"""
|
||||||
now = datetime.now()
|
now = datetime.now(self._current_tz)
|
||||||
delta_temp = (
|
delta_temp = (
|
||||||
now - self._last_temperature_mesure.replace(tzinfo=None)
|
now - self._last_temperature_mesure.replace(tzinfo=self._current_tz)
|
||||||
).total_seconds() / 60.0
|
).total_seconds() / 60.0
|
||||||
delta_ext_temp = (
|
delta_ext_temp = (
|
||||||
now - self._last_ext_temperature_mesure.replace(tzinfo=None)
|
now - self._last_ext_temperature_mesure.replace(tzinfo=self._current_tz)
|
||||||
).total_seconds() / 60.0
|
).total_seconds() / 60.0
|
||||||
|
|
||||||
mode_cond = self._is_over_climate or self._hvac_mode != HVACMode.OFF
|
mode_cond = self._is_over_climate or self._hvac_mode != HVACMode.OFF
|
||||||
@@ -1796,8 +1941,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self.send_event(
|
self.send_event(
|
||||||
EventType.TEMPERATURE_EVENT,
|
EventType.TEMPERATURE_EVENT,
|
||||||
{
|
{
|
||||||
"last_temperature_mesure": self._last_temperature_mesure.isoformat(),
|
"last_temperature_mesure": self._last_temperature_mesure.replace(
|
||||||
"last_ext_temperature_mesure": self._last_ext_temperature_mesure.isoformat(),
|
tzinfo=self._current_tz
|
||||||
|
).isoformat(),
|
||||||
|
"last_ext_temperature_mesure": self._last_ext_temperature_mesure.replace(
|
||||||
|
tzinfo=self._current_tz
|
||||||
|
).isoformat(),
|
||||||
"current_temp": self._cur_temp,
|
"current_temp": self._cur_temp,
|
||||||
"current_ext_temp": self._cur_ext_temp,
|
"current_ext_temp": self._cur_ext_temp,
|
||||||
"target_temp": self.target_temperature,
|
"target_temp": self.target_temperature,
|
||||||
@@ -1819,8 +1968,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
EventType.SECURITY_EVENT,
|
EventType.SECURITY_EVENT,
|
||||||
{
|
{
|
||||||
"type": "start",
|
"type": "start",
|
||||||
"last_temperature_mesure": self._last_temperature_mesure.isoformat(),
|
"last_temperature_mesure": self._last_temperature_mesure.replace(
|
||||||
"last_ext_temperature_mesure": self._last_ext_temperature_mesure.isoformat(),
|
tzinfo=self._current_tz
|
||||||
|
).isoformat(),
|
||||||
|
"last_ext_temperature_mesure": self._last_ext_temperature_mesure.replace(
|
||||||
|
tzinfo=self._current_tz
|
||||||
|
).isoformat(),
|
||||||
"current_temp": self._cur_temp,
|
"current_temp": self._cur_temp,
|
||||||
"current_ext_temp": self._cur_ext_temp,
|
"current_ext_temp": self._cur_ext_temp,
|
||||||
"target_temp": self.target_temperature,
|
"target_temp": self.target_temperature,
|
||||||
@@ -1849,8 +2002,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
EventType.SECURITY_EVENT,
|
EventType.SECURITY_EVENT,
|
||||||
{
|
{
|
||||||
"type": "end",
|
"type": "end",
|
||||||
"last_temperature_mesure": self._last_temperature_mesure.isoformat(),
|
"last_temperature_mesure": self._last_temperature_mesure.replace(
|
||||||
"last_ext_temperature_mesure": self._last_ext_temperature_mesure.isoformat(),
|
tzinfo=self._current_tz
|
||||||
|
).isoformat(),
|
||||||
|
"last_ext_temperature_mesure": self._last_ext_temperature_mesure.replace(
|
||||||
|
tzinfo=self._current_tz
|
||||||
|
).isoformat(),
|
||||||
"current_temp": self._cur_temp,
|
"current_temp": self._cur_temp,
|
||||||
"current_ext_temp": self._cur_ext_temp,
|
"current_ext_temp": self._cur_ext_temp,
|
||||||
"target_temp": self.target_temperature,
|
"target_temp": self.target_temperature,
|
||||||
@@ -1964,7 +2121,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s - No action on heater cause duration is 0", self
|
"%s - No action on heater cause duration is 0", self
|
||||||
)
|
)
|
||||||
self.update_custom_attributes()
|
|
||||||
self._async_cancel_cycle = async_call_later(
|
self._async_cancel_cycle = async_call_later(
|
||||||
self.hass,
|
self.hass,
|
||||||
time,
|
time,
|
||||||
@@ -1978,6 +2134,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
heater_action=self._async_heater_turn_on,
|
heater_action=self._async_heater_turn_on,
|
||||||
next_cycle_action=_turn_off_later,
|
next_cycle_action=_turn_off_later,
|
||||||
)
|
)
|
||||||
|
self.update_custom_attributes()
|
||||||
|
|
||||||
async def _turn_off_later(_):
|
async def _turn_off_later(_):
|
||||||
await _turn_on_off_later(
|
await _turn_on_off_later(
|
||||||
@@ -1986,6 +2143,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
heater_action=self._async_underlying_entity_turn_off,
|
heater_action=self._async_underlying_entity_turn_off,
|
||||||
next_cycle_action=_turn_on_later,
|
next_cycle_action=_turn_on_later,
|
||||||
)
|
)
|
||||||
|
# increment energy at the end of the cycle
|
||||||
|
self.incremente_energy()
|
||||||
|
self.update_custom_attributes()
|
||||||
|
|
||||||
await _turn_on_later(None)
|
await _turn_on_later(None)
|
||||||
|
|
||||||
@@ -2018,11 +2178,32 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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"""
|
||||||
|
|
||||||
self._attr_extra_state_attributes: dict(str, str) = {
|
self._attr_extra_state_attributes: dict(str, str) = {
|
||||||
"hvac_mode": self._hvac_mode,
|
"hvac_mode": self.hvac_mode,
|
||||||
|
"preset_mode": self.preset_mode,
|
||||||
"type": self._thermostat_type,
|
"type": self._thermostat_type,
|
||||||
"eco_temp": self._presets[PRESET_ECO],
|
"eco_temp": self._presets[PRESET_ECO],
|
||||||
"boost_temp": self._presets[PRESET_BOOST],
|
"boost_temp": self._presets[PRESET_BOOST],
|
||||||
@@ -2050,16 +2231,29 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
"security_delay_min": self._security_delay_min,
|
"security_delay_min": self._security_delay_min,
|
||||||
"security_min_on_percent": self._security_min_on_percent,
|
"security_min_on_percent": self._security_min_on_percent,
|
||||||
"security_default_on_percent": self._security_default_on_percent,
|
"security_default_on_percent": self._security_default_on_percent,
|
||||||
"last_temperature_datetime": self._last_temperature_mesure.isoformat(),
|
"last_temperature_datetime": self._last_temperature_mesure.astimezone(
|
||||||
"last_ext_temperature_datetime": self._last_ext_temperature_mesure.isoformat(),
|
self._current_tz
|
||||||
|
).isoformat(),
|
||||||
|
"last_ext_temperature_datetime": self._last_ext_temperature_mesure.astimezone(
|
||||||
|
self._current_tz
|
||||||
|
).isoformat(),
|
||||||
"security_state": self._security_state,
|
"security_state": self._security_state,
|
||||||
"minimal_activation_delay_sec": self._minimal_activation_delay,
|
"minimal_activation_delay_sec": self._minimal_activation_delay,
|
||||||
"last_update_datetime": datetime.now().isoformat(),
|
"device_power": self._device_power,
|
||||||
|
ATTR_MEAN_POWER_CYCLE: self.mean_cycle_power,
|
||||||
|
ATTR_TOTAL_ENERGY: self.total_energy,
|
||||||
|
"last_update_datetime": datetime.now()
|
||||||
|
.astimezone(self._current_tz)
|
||||||
|
.isoformat(),
|
||||||
|
"timezone": str(self._current_tz),
|
||||||
}
|
}
|
||||||
if self._is_over_climate:
|
if self._is_over_climate:
|
||||||
self._attr_extra_state_attributes[
|
self._attr_extra_state_attributes[
|
||||||
"underlying_climate"
|
"underlying_climate"
|
||||||
] = self._climate_entity_id
|
] = self._climate_entity_id
|
||||||
|
self._attr_extra_state_attributes[
|
||||||
|
"start_hvac_action_date"
|
||||||
|
] = self._underlying_climate_start_hvac_action_date
|
||||||
else:
|
else:
|
||||||
self._attr_extra_state_attributes[
|
self._attr_extra_state_attributes[
|
||||||
"underlying_switch"
|
"underlying_switch"
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
|
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
|
||||||
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
|
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
|
||||||
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
|
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
|
||||||
|
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
|
||||||
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
|
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
|
||||||
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
|
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
|
||||||
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
|
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
|
||||||
@@ -290,7 +291,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
|
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
|
||||||
),
|
),
|
||||||
), # vol.In(power_sensors),
|
), # vol.In(power_sensors),
|
||||||
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
|
|
||||||
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
|
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -135,6 +135,9 @@ SERVICE_SET_SECURITY = "set_security"
|
|||||||
DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
|
DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
|
||||||
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
|
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
|
||||||
|
|
||||||
|
ATTR_TOTAL_ENERGY = "total_energy"
|
||||||
|
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"
|
||||||
|
|
||||||
|
|
||||||
class EventType(Enum):
|
class EventType(Enum):
|
||||||
"""The event type that can be sent"""
|
"""The event type that can be sent"""
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"version": "0.0.1",
|
|
||||||
"domain": "versatile_thermostat",
|
"domain": "versatile_thermostat",
|
||||||
"name": "Versatile Thermostat",
|
"name": "Versatile Thermostat",
|
||||||
"config_flow": true,
|
|
||||||
"documentation": "https://github.com/jmcollin78/versatile_thermostat",
|
|
||||||
"issue_tracker": "https://github.com/jmcollin78/versatile_thermostat/issues",
|
|
||||||
"requirements": [],
|
|
||||||
"ssdp": [],
|
|
||||||
"zeroconf": [],
|
|
||||||
"homekit": {},
|
|
||||||
"dependencies": [],
|
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@jmcollin78"
|
"@jmcollin78"
|
||||||
],
|
],
|
||||||
"quality_scale": "silver",
|
"config_flow": true,
|
||||||
|
"dependencies": [],
|
||||||
|
"documentation": "https://github.com/jmcollin78/versatile_thermostat",
|
||||||
|
"homekit": {},
|
||||||
|
"integration_type": "device",
|
||||||
"iot_class": "calculated",
|
"iot_class": "calculated",
|
||||||
"integration_type": "device"
|
"issue_tracker": "https://github.com/jmcollin78/versatile_thermostat/issues",
|
||||||
|
"quality_scale": "silver",
|
||||||
|
"requirements": [],
|
||||||
|
"ssdp": [],
|
||||||
|
"version": "0.0.1",
|
||||||
|
"zeroconf": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
""" The TPI calculation module """
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -21,7 +22,7 @@ class PropAlgorithm:
|
|||||||
tpi_coef_ext,
|
tpi_coef_ext,
|
||||||
cycle_min: int,
|
cycle_min: int,
|
||||||
minimal_activation_delay: int,
|
minimal_activation_delay: int,
|
||||||
):
|
) -> None:
|
||||||
"""Initialisation of the Proportional Algorithm"""
|
"""Initialisation of the Proportional Algorithm"""
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d",
|
"Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"cycle_min": "Cycle duration (minutes)",
|
"cycle_min": "Cycle duration (minutes)",
|
||||||
"temp_min": "Minimal temperature allowed",
|
"temp_min": "Minimal temperature allowed",
|
||||||
"temp_max": "Maximal temperature allowed",
|
"temp_max": "Maximal temperature allowed",
|
||||||
|
"device_power": "Device power (kW)",
|
||||||
"use_window_feature": "Use window detection",
|
"use_window_feature": "Use window detection",
|
||||||
"use_motion_feature": "Use motion detection",
|
"use_motion_feature": "Use motion detection",
|
||||||
"use_power_feature": "Use power management",
|
"use_power_feature": "Use power management",
|
||||||
@@ -70,7 +71,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"power_sensor_entity_id": "Power sensor entity id",
|
"power_sensor_entity_id": "Power sensor entity id",
|
||||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||||
"device_power": "Device power (kW)",
|
|
||||||
"power_temp": "Temperature for Power shedding"
|
"power_temp": "Temperature for Power shedding"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -117,6 +117,7 @@
|
|||||||
"cycle_min": "Cycle duration (minutes)",
|
"cycle_min": "Cycle duration (minutes)",
|
||||||
"temp_min": "Minimal temperature allowed",
|
"temp_min": "Minimal temperature allowed",
|
||||||
"temp_max": "Maximal temperature allowed",
|
"temp_max": "Maximal temperature allowed",
|
||||||
|
"device_power": "Device power (kW)",
|
||||||
"use_window_feature": "Use window detection",
|
"use_window_feature": "Use window detection",
|
||||||
"use_motion_feature": "Use motion detection",
|
"use_motion_feature": "Use motion detection",
|
||||||
"use_power_feature": "Use power management",
|
"use_power_feature": "Use power management",
|
||||||
@@ -173,7 +174,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"power_sensor_entity_id": "Power sensor entity id",
|
"power_sensor_entity_id": "Power sensor entity id",
|
||||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||||
"device_power": "Device power (kW)",
|
|
||||||
"power_temp": "Temperature for Power shedding"
|
"power_temp": "Temperature for Power shedding"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
""" Some common resources """
|
""" Some common resources """
|
||||||
from unittest.mock import patch
|
from typing import Mapping
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
|
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
|
||||||
from homeassistant.const import UnitOfTemperature
|
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
from ..climate import VersatileThermostat
|
from ..climate import VersatileThermostat
|
||||||
from ..const import DOMAIN, PRESET_SECURITY, PRESET_POWER, EventType
|
from ..const import *
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
@@ -17,6 +19,7 @@ from homeassistant.components.climate import (
|
|||||||
ATTR_PRESET_MODE,
|
ATTR_PRESET_MODE,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
HVACAction,
|
HVACAction,
|
||||||
|
ClimateEntityFeature,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
@@ -77,6 +80,64 @@ class MockClimate(ClimateEntity):
|
|||||||
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
|
|
||||||
|
|
||||||
|
class MagicMockClimate(MagicMock):
|
||||||
|
@property
|
||||||
|
def temperature_unit(self):
|
||||||
|
return UnitOfTemperature.CELSIUS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hvac_mode(self):
|
||||||
|
return HVACMode.HEAT
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hvac_action(self):
|
||||||
|
return HVACAction.IDLE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature(self):
|
||||||
|
return 15
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_temperature(self):
|
||||||
|
return 14
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature_step(self) -> float | None:
|
||||||
|
return 0.5
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature_high(self) -> float | None:
|
||||||
|
return 35
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature_low(self) -> float | None:
|
||||||
|
return 7
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hvac_modes(self) -> list[str] | None:
|
||||||
|
return [HVACMode.HEAT, HVACMode.OFF, HVACMode.COOL]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_modes(self) -> list[str] | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def swing_modes(self) -> list[str] | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_mode(self) -> str | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def swing_mode(self) -> str | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
return ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
|
||||||
|
|
||||||
async def create_thermostat(
|
async def create_thermostat(
|
||||||
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
||||||
) -> VersatileThermostat:
|
) -> VersatileThermostat:
|
||||||
@@ -113,4 +174,98 @@ async def send_temperature_change_event(entity: VersatileThermostat, new_temp, d
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await entity._async_temperature_changed(temp_event)
|
return await entity._async_temperature_changed(temp_event)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_power_change_event(entity: VersatileThermostat, new_power, date):
|
||||||
|
"""Sending a new power event simulating a change on power sensor"""
|
||||||
|
power_event = Event(
|
||||||
|
EVENT_STATE_CHANGED,
|
||||||
|
{
|
||||||
|
"new_state": State(
|
||||||
|
entity_id=entity.entity_id,
|
||||||
|
state=new_power,
|
||||||
|
last_changed=date,
|
||||||
|
last_updated=date,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return await entity._async_power_changed(power_event)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_max_power_change_event(entity: VersatileThermostat, new_power_max, date):
|
||||||
|
"""Sending a new power max event simulating a change on power max sensor"""
|
||||||
|
power_event = Event(
|
||||||
|
EVENT_STATE_CHANGED,
|
||||||
|
{
|
||||||
|
"new_state": State(
|
||||||
|
entity_id=entity.entity_id,
|
||||||
|
state=new_power_max,
|
||||||
|
last_changed=date,
|
||||||
|
last_updated=date,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return await entity._async_max_power_changed(power_event)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_window_change_event(
|
||||||
|
entity: VersatileThermostat, new_state: bool, old_state: bool, date
|
||||||
|
):
|
||||||
|
"""Sending a new window event simulating a change on the window state"""
|
||||||
|
window_event = Event(
|
||||||
|
EVENT_STATE_CHANGED,
|
||||||
|
{
|
||||||
|
"new_state": State(
|
||||||
|
entity_id=entity.entity_id,
|
||||||
|
state=STATE_ON if new_state else STATE_OFF,
|
||||||
|
last_changed=date,
|
||||||
|
last_updated=date,
|
||||||
|
),
|
||||||
|
"old_state": State(
|
||||||
|
entity_id=entity.entity_id,
|
||||||
|
state=STATE_ON if old_state else STATE_OFF,
|
||||||
|
last_changed=date,
|
||||||
|
last_updated=date,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ret = await entity._async_windows_changed(window_event)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_tz(hass):
|
||||||
|
"""Get the current timezone"""
|
||||||
|
|
||||||
|
return dt_util.get_time_zone(hass.config.time_zone)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_climate_change_event(
|
||||||
|
entity: VersatileThermostat,
|
||||||
|
new_hvac_mode: HVACMode,
|
||||||
|
old_hvac_mode: HVACMode,
|
||||||
|
new_hvac_action: HVACAction,
|
||||||
|
old_hvac_action: HVACAction,
|
||||||
|
date,
|
||||||
|
):
|
||||||
|
"""Sending a new climate event simulating a change on the underlying climate state"""
|
||||||
|
climate_event = Event(
|
||||||
|
EVENT_STATE_CHANGED,
|
||||||
|
{
|
||||||
|
"new_state": State(
|
||||||
|
entity_id=entity.entity_id,
|
||||||
|
state=new_hvac_mode,
|
||||||
|
attributes={"hvac_action": new_hvac_action},
|
||||||
|
last_changed=date,
|
||||||
|
last_updated=date,
|
||||||
|
),
|
||||||
|
"old_state": State(
|
||||||
|
entity_id=entity.entity_id,
|
||||||
|
state=old_hvac_mode,
|
||||||
|
attributes={"hvac_action": old_hvac_action},
|
||||||
|
last_changed=date,
|
||||||
|
last_updated=date,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ret = await entity._async_climate_changed(climate_event)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
|||||||
CONF_CYCLE_MIN: 5,
|
CONF_CYCLE_MIN: 5,
|
||||||
CONF_TEMP_MIN: 15,
|
CONF_TEMP_MIN: 15,
|
||||||
CONF_TEMP_MAX: 30,
|
CONF_TEMP_MAX: 30,
|
||||||
|
CONF_DEVICE_POWER: 1,
|
||||||
CONF_USE_WINDOW_FEATURE: True,
|
CONF_USE_WINDOW_FEATURE: True,
|
||||||
CONF_USE_MOTION_FEATURE: True,
|
CONF_USE_MOTION_FEATURE: True,
|
||||||
CONF_USE_POWER_FEATURE: True,
|
CONF_USE_POWER_FEATURE: True,
|
||||||
@@ -65,6 +66,7 @@ MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
|
|||||||
CONF_CYCLE_MIN: 5,
|
CONF_CYCLE_MIN: 5,
|
||||||
CONF_TEMP_MIN: 15,
|
CONF_TEMP_MIN: 15,
|
||||||
CONF_TEMP_MAX: 30,
|
CONF_TEMP_MAX: 30,
|
||||||
|
CONF_DEVICE_POWER: 1,
|
||||||
# Keep default values which are False
|
# Keep default values which are False
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +105,6 @@ MOCK_MOTION_CONFIG = {
|
|||||||
MOCK_POWER_CONFIG = {
|
MOCK_POWER_CONFIG = {
|
||||||
CONF_POWER_SENSOR: "sensor.power_sensor",
|
CONF_POWER_SENSOR: "sensor.power_sensor",
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
|
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
|
||||||
CONF_DEVICE_POWER: 1,
|
|
||||||
CONF_PRESET_POWER: 10,
|
CONF_PRESET_POWER: 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,447 @@
|
|||||||
|
""" Test the Power management """
|
||||||
|
from unittest.mock import patch, call, MagicMock
|
||||||
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from homeassistant.const import UnitOfTemperature
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_power_management_hvac_off(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state
|
||||||
|
):
|
||||||
|
"""Test the Power management"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||||
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
|
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 15,
|
||||||
|
CONF_TEMP_MAX: 30,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_HEATER: "switch.mock_switch",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||||
|
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||||
|
CONF_DEVICE_POWER: 100,
|
||||||
|
CONF_PRESET_POWER: "eco",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
tpi_algo = entity._prop_algorithm
|
||||||
|
assert tpi_algo
|
||||||
|
|
||||||
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.target_temperature == 19
|
||||||
|
assert entity.overpowering_state is None
|
||||||
|
assert entity.hvac_mode == HVACMode.OFF
|
||||||
|
|
||||||
|
# Send power mesurement
|
||||||
|
await send_power_change_event(entity, 50, datetime.now())
|
||||||
|
assert await entity.check_overpowering() is False
|
||||||
|
|
||||||
|
# All configuration is not complete
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.overpowering_state is None
|
||||||
|
|
||||||
|
# Send power max mesurement
|
||||||
|
await send_max_power_change_event(entity, 300, datetime.now())
|
||||||
|
assert await entity.check_overpowering() is False
|
||||||
|
# All configuration is complete and power is < power_max
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.overpowering_state is False
|
||||||
|
|
||||||
|
# Send power max mesurement too low but HVACMode is off
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
) as mock_send_event, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||||
|
) as mock_heater_on, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||||
|
) as mock_heater_off:
|
||||||
|
await send_max_power_change_event(entity, 149, datetime.now())
|
||||||
|
assert await entity.check_overpowering() is True
|
||||||
|
# All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.overpowering_state is True
|
||||||
|
|
||||||
|
assert mock_send_event.call_count == 0
|
||||||
|
assert mock_heater_on.call_count == 0
|
||||||
|
assert mock_heater_off.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is_state):
|
||||||
|
"""Test the Power management"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||||
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
|
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 15,
|
||||||
|
CONF_TEMP_MAX: 30,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_HEATER: "switch.mock_switch",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||||
|
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||||
|
CONF_DEVICE_POWER: 100,
|
||||||
|
CONF_PRESET_POWER: 12,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
tpi_algo = entity._prop_algorithm
|
||||||
|
assert tpi_algo
|
||||||
|
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.overpowering_state is None
|
||||||
|
assert entity.target_temperature == 19
|
||||||
|
|
||||||
|
# Send power mesurement
|
||||||
|
await send_power_change_event(entity, 50, datetime.now())
|
||||||
|
# Send power max mesurement
|
||||||
|
await send_max_power_change_event(entity, 300, datetime.now())
|
||||||
|
assert await entity.check_overpowering() is False
|
||||||
|
# All configuration is complete and power is < power_max
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.overpowering_state is False
|
||||||
|
|
||||||
|
# Send power max mesurement too low and HVACMode is on
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
) as mock_send_event, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||||
|
) as mock_heater_on, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||||
|
) as mock_heater_off:
|
||||||
|
await send_max_power_change_event(entity, 149, datetime.now())
|
||||||
|
assert await entity.check_overpowering() is True
|
||||||
|
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||||
|
assert entity.preset_mode is PRESET_POWER
|
||||||
|
assert entity.overpowering_state is True
|
||||||
|
assert entity.target_temperature == 12
|
||||||
|
|
||||||
|
assert mock_send_event.call_count == 2
|
||||||
|
mock_send_event.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_POWER}),
|
||||||
|
call.send_event(
|
||||||
|
EventType.POWER_EVENT,
|
||||||
|
{
|
||||||
|
"type": "start",
|
||||||
|
"current_power": 50,
|
||||||
|
"device_power": 100,
|
||||||
|
"current_power_max": 149,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
any_order=True,
|
||||||
|
)
|
||||||
|
assert mock_heater_on.call_count == 0
|
||||||
|
assert mock_heater_off.call_count == 1
|
||||||
|
|
||||||
|
# Send power mesurement low to unseet power preset
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
) as mock_send_event, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||||
|
) as mock_heater_on, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||||
|
) as mock_heater_off:
|
||||||
|
await send_power_change_event(entity, 48, datetime.now())
|
||||||
|
assert await entity.check_overpowering() is False
|
||||||
|
# All configuration is complete and power is < power_max, we restore previous preset
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.overpowering_state is False
|
||||||
|
assert entity.target_temperature == 19
|
||||||
|
|
||||||
|
assert mock_send_event.call_count == 2
|
||||||
|
mock_send_event.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_BOOST}),
|
||||||
|
call.send_event(
|
||||||
|
EventType.POWER_EVENT,
|
||||||
|
{
|
||||||
|
"type": "end",
|
||||||
|
"current_power": 48,
|
||||||
|
"device_power": 100,
|
||||||
|
"current_power_max": 149,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
any_order=True,
|
||||||
|
)
|
||||||
|
# No current temperature is set so the heater wont be turned on
|
||||||
|
assert mock_heater_on.call_count == 0
|
||||||
|
assert mock_heater_off.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_power_management_energy_over_switch(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state
|
||||||
|
):
|
||||||
|
"""Test the Power management energy mesurement"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||||
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
|
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 15,
|
||||||
|
CONF_TEMP_MAX: 30,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_HEATER: "switch.mock_switch",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||||
|
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||||
|
CONF_DEVICE_POWER: 100,
|
||||||
|
CONF_PRESET_POWER: 12,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
tpi_algo = entity._prop_algorithm
|
||||||
|
assert tpi_algo
|
||||||
|
|
||||||
|
assert entity.total_energy == 0
|
||||||
|
|
||||||
|
# set temperature to 15 so that on_percent will be set
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
) as mock_send_event, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||||
|
) as mock_heater_on, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||||
|
) as mock_heater_off:
|
||||||
|
await send_temperature_change_event(entity, 15, datetime.now())
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
|
|
||||||
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.target_temperature == 19
|
||||||
|
assert entity.current_temperature == 15
|
||||||
|
assert tpi_algo.on_percent == 1
|
||||||
|
|
||||||
|
assert entity.mean_cycle_power == 100.0
|
||||||
|
|
||||||
|
assert mock_send_event.call_count == 2
|
||||||
|
assert mock_heater_on.call_count == 1
|
||||||
|
assert mock_heater_off.call_count == 0
|
||||||
|
|
||||||
|
entity.incremente_energy()
|
||||||
|
assert entity.total_energy == 100 * 5 / 60.0
|
||||||
|
entity.incremente_energy()
|
||||||
|
assert entity.total_energy == 2 * 100 * 5 / 60.0
|
||||||
|
|
||||||
|
# change temperature to a higher value
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
) as mock_send_event, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||||
|
) as mock_heater_on, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||||
|
) as mock_heater_off:
|
||||||
|
await send_temperature_change_event(entity, 18, datetime.now())
|
||||||
|
assert tpi_algo.on_percent == 0.3
|
||||||
|
assert entity.mean_cycle_power == 30.0
|
||||||
|
|
||||||
|
assert mock_send_event.call_count == 0
|
||||||
|
assert mock_heater_on.call_count == 0
|
||||||
|
assert mock_heater_off.call_count == 0
|
||||||
|
|
||||||
|
entity.incremente_energy()
|
||||||
|
assert round(entity.total_energy, 2) == round((2.0 + 0.3) * 100 * 5 / 60.0, 2)
|
||||||
|
|
||||||
|
entity.incremente_energy()
|
||||||
|
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
|
||||||
|
|
||||||
|
# change temperature to a much higher value so that heater will be shut down
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
) as mock_send_event, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||||
|
) as mock_heater_on, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||||
|
) as mock_heater_off:
|
||||||
|
await send_temperature_change_event(entity, 20, datetime.now())
|
||||||
|
assert tpi_algo.on_percent == 0.0
|
||||||
|
assert entity.mean_cycle_power == 0.0
|
||||||
|
|
||||||
|
assert mock_send_event.call_count == 0
|
||||||
|
assert mock_heater_on.call_count == 0
|
||||||
|
assert mock_heater_off.call_count == 0
|
||||||
|
|
||||||
|
entity.incremente_energy()
|
||||||
|
# No change on energy
|
||||||
|
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
|
||||||
|
|
||||||
|
# Still no change
|
||||||
|
entity.incremente_energy()
|
||||||
|
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_power_management_energy_over_climate(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state
|
||||||
|
):
|
||||||
|
"""Test the Power management for a over_climate thermostat"""
|
||||||
|
|
||||||
|
the_mock_underlying = MagicMockClimate()
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||||
|
return_value=the_mock_underlying,
|
||||||
|
):
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverClimateMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverClimateMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||||
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
|
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 15,
|
||||||
|
CONF_TEMP_MAX: 30,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_CLIMATE: "climate.mock_climate",
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||||
|
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||||
|
CONF_DEVICE_POWER: 100,
|
||||||
|
CONF_PRESET_POWER: 12,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverclimatemockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
assert entity._is_over_climate
|
||||||
|
|
||||||
|
now = datetime.now(tz=get_tz(hass))
|
||||||
|
await send_temperature_change_event(entity, 15, now)
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
|
|
||||||
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
|
assert entity.hvac_action is HVACAction.IDLE
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.target_temperature == 19
|
||||||
|
assert entity.current_temperature == 15
|
||||||
|
|
||||||
|
# Not initialised yet
|
||||||
|
assert entity.mean_cycle_power is None
|
||||||
|
assert entity._underlying_climate_start_hvac_action_date is None
|
||||||
|
|
||||||
|
# Send a climate_change event with HVACAction=HEATING
|
||||||
|
event_timestamp = now - timedelta(minutes=3)
|
||||||
|
await send_climate_change_event(
|
||||||
|
entity,
|
||||||
|
new_hvac_mode=HVACMode.HEAT,
|
||||||
|
old_hvac_mode=HVACMode.HEAT,
|
||||||
|
new_hvac_action=HVACAction.HEATING,
|
||||||
|
old_hvac_action=HVACAction.OFF,
|
||||||
|
date=event_timestamp,
|
||||||
|
)
|
||||||
|
# We have the start event and not the end event
|
||||||
|
assert (entity._underlying_climate_start_hvac_action_date - now).total_seconds() < 1
|
||||||
|
|
||||||
|
entity.incremente_energy()
|
||||||
|
assert entity.total_energy == 0
|
||||||
|
|
||||||
|
# Send a climate_change event with HVACAction=IDLE (end of heating)
|
||||||
|
await send_climate_change_event(
|
||||||
|
entity,
|
||||||
|
new_hvac_mode=HVACMode.HEAT,
|
||||||
|
old_hvac_mode=HVACMode.HEAT,
|
||||||
|
new_hvac_action=HVACAction.IDLE,
|
||||||
|
old_hvac_action=HVACAction.HEATING,
|
||||||
|
date=now,
|
||||||
|
)
|
||||||
|
# We have the end event -> we should have some power and on_percent
|
||||||
|
assert entity._underlying_climate_start_hvac_action_date is None
|
||||||
|
|
||||||
|
# 3 minutes at 100 W
|
||||||
|
assert entity.total_energy == 100 * 3.0 / 60
|
||||||
|
|
||||||
|
# Test the re-increment
|
||||||
|
entity.incremente_energy()
|
||||||
|
assert entity.total_energy == 2 * 100 * 3.0 / 60
|
||||||
@@ -19,6 +19,8 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
6. check that security is off and preset is changed to boost
|
6. check that security is off and preset is changed to boost
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
tz = get_tz(hass)
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
title="TheOverSwitchMockName",
|
title="TheOverSwitchMockName",
|
||||||
@@ -50,7 +52,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 1. creates a thermostat and check that security is off
|
# 1. creates a thermostat and check that security is off
|
||||||
now: datetime = datetime.now()
|
now: datetime = datetime.now(tz=tz)
|
||||||
entity: VersatileThermostat = await create_thermostat(
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
hass, entry, "climate.theoverswitchmockname"
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
)
|
)
|
||||||
@@ -66,8 +68,10 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
]
|
]
|
||||||
assert entity._last_ext_temperature_mesure is not None
|
assert entity._last_ext_temperature_mesure is not None
|
||||||
assert entity._last_temperature_mesure is not None
|
assert entity._last_temperature_mesure is not None
|
||||||
assert (entity._last_temperature_mesure - now).total_seconds() < 1
|
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
|
||||||
assert (entity._last_ext_temperature_mesure - now).total_seconds() < 1
|
assert (
|
||||||
|
entity._last_ext_temperature_mesure.astimezone(tz) - now
|
||||||
|
).total_seconds() < 1
|
||||||
|
|
||||||
# set a preset
|
# set a preset
|
||||||
assert entity.preset_mode is PRESET_NONE
|
assert entity.preset_mode is PRESET_NONE
|
||||||
@@ -166,8 +170,12 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
EventType.SECURITY_EVENT,
|
EventType.SECURITY_EVENT,
|
||||||
{
|
{
|
||||||
"type": "end",
|
"type": "end",
|
||||||
"last_temperature_mesure": event_timestamp.isoformat(),
|
"last_temperature_mesure": event_timestamp.astimezone(
|
||||||
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.isoformat(),
|
tz
|
||||||
|
).isoformat(),
|
||||||
|
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.astimezone(
|
||||||
|
tz
|
||||||
|
).isoformat(),
|
||||||
"current_temp": 15.2,
|
"current_temp": 15.2,
|
||||||
"current_ext_temp": None,
|
"current_ext_temp": None,
|
||||||
"target_temp": 19,
|
"target_temp": 19,
|
||||||
|
|||||||
@@ -11,24 +11,25 @@ async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
title="TheOverSwitchMockName",
|
title="TheOverSwitchMockName",
|
||||||
unique_id="uniqueId",
|
unique_id="uniqueId",
|
||||||
data={
|
data={
|
||||||
"name": "TheOverSwitchMockName",
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
"thermostat_type": "thermostat_over_switch",
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||||
"temperature_sensor_entity_id": "sensor.mock_temp_sensor",
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
"external_temperature_sensor_entity_id": "sensor.mock_ext_temp_sensor",
|
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||||
"cycle_min": 5,
|
CONF_CYCLE_MIN: 5,
|
||||||
"temp_min": 15,
|
CONF_TEMP_MIN: 15,
|
||||||
"temp_max": 30,
|
CONF_TEMP_MAX: 30,
|
||||||
"use_window_feature": False,
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
"use_motion_feature": False,
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
"use_power_feature": False,
|
CONF_USE_POWER_FEATURE: False,
|
||||||
"use_presence_feature": False,
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
"heater_entity_id": "switch.mock_switch",
|
CONF_HEATER: "switch.mock_switch",
|
||||||
"proportional_function": "tpi",
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
"tpi_coef_int": 0.3,
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
"tpi_coef_ext": 0.01,
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
"minimal_activation_delay": 30,
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
"security_delay_min": 5,
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
"security_default_on_percent": 0.3,
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
# CONF_DEVICE_POWER: 100,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
assert tpi_algo.calculated_on_percent == 1
|
assert tpi_algo.calculated_on_percent == 1
|
||||||
assert tpi_algo.on_time_sec == 300
|
assert tpi_algo.on_time_sec == 300
|
||||||
assert tpi_algo.off_time_sec == 0
|
assert tpi_algo.off_time_sec == 0
|
||||||
|
assert entity.mean_cycle_power is None # no device power configured
|
||||||
|
|
||||||
tpi_algo.calculate(15, 14, 5)
|
tpi_algo.calculate(15, 14, 5)
|
||||||
assert tpi_algo.on_percent == 0.4
|
assert tpi_algo.on_percent == 0.4
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
""" Test the Window management """
|
||||||
|
from unittest.mock import patch, call
|
||||||
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_window_management_time_not_enough(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state
|
||||||
|
):
|
||||||
|
"""Test the Power management"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||||
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
|
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 15,
|
||||||
|
CONF_TEMP_MAX: 30,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: True,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_HEATER: "switch.mock_switch",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||||
|
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
tpi_algo = entity._prop_algorithm
|
||||||
|
assert tpi_algo
|
||||||
|
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.overpowering_state is None
|
||||||
|
assert entity.target_temperature == 19
|
||||||
|
|
||||||
|
assert entity.window_state is None
|
||||||
|
|
||||||
|
# Open the window, but condition of time is not satisfied and check the thermostat don't turns off
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
) as mock_send_event, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||||
|
) as mock_heater_on, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||||
|
) as mock_heater_off, patch(
|
||||||
|
"homeassistant.helpers.condition.state", return_value=False
|
||||||
|
) as mock_condition:
|
||||||
|
await send_temperature_change_event(entity, 15, datetime.now())
|
||||||
|
try_window_condition = await send_window_change_event(
|
||||||
|
entity, True, False, datetime.now()
|
||||||
|
)
|
||||||
|
# simulate the call to try_window_condition
|
||||||
|
await try_window_condition(None)
|
||||||
|
|
||||||
|
assert mock_send_event.call_count == 0
|
||||||
|
assert mock_heater_on.call_count == 1
|
||||||
|
assert mock_heater_off.call_count == 0
|
||||||
|
assert mock_condition.call_count == 1
|
||||||
|
|
||||||
|
assert entity.window_state == STATE_OFF
|
||||||
|
|
||||||
|
# Close the window
|
||||||
|
try_window_condition = await send_window_change_event(
|
||||||
|
entity, False, False, datetime.now()
|
||||||
|
)
|
||||||
|
# simulate the call to try_window_condition
|
||||||
|
await try_window_condition(None)
|
||||||
|
assert entity.window_state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_window_management_time_enough(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state
|
||||||
|
):
|
||||||
|
"""Test the Power management"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||||
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
|
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 15,
|
||||||
|
CONF_TEMP_MAX: 30,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: True,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_HEATER: "switch.mock_switch",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||||
|
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
tpi_algo = entity._prop_algorithm
|
||||||
|
assert tpi_algo
|
||||||
|
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.overpowering_state is None
|
||||||
|
assert entity.target_temperature == 19
|
||||||
|
|
||||||
|
assert entity.window_state is None
|
||||||
|
|
||||||
|
# Open the window, but condition of time is not satisfied and check the thermostat don't turns off
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
) as mock_send_event, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||||
|
) as mock_heater_on, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||||
|
) as mock_heater_off, patch(
|
||||||
|
"homeassistant.helpers.condition.state", return_value=True
|
||||||
|
) as mock_condition, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
await send_temperature_change_event(entity, 15, datetime.now())
|
||||||
|
try_window_condition = await send_window_change_event(
|
||||||
|
entity, True, False, datetime.now()
|
||||||
|
)
|
||||||
|
# simulate the call to try_window_condition
|
||||||
|
await try_window_condition(None)
|
||||||
|
|
||||||
|
assert mock_send_event.call_count == 1
|
||||||
|
mock_send_event.assert_has_calls(
|
||||||
|
[call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF})]
|
||||||
|
)
|
||||||
|
assert mock_heater_on.call_count == 1
|
||||||
|
# One call in turn_oiff and one call in the control_heating
|
||||||
|
assert mock_heater_off.call_count == 2
|
||||||
|
assert mock_condition.call_count == 1
|
||||||
|
|
||||||
|
assert entity.window_state == STATE_ON
|
||||||
|
|
||||||
|
# Close the window
|
||||||
|
try_window_condition = await send_window_change_event(
|
||||||
|
entity, False, True, datetime.now()
|
||||||
|
)
|
||||||
|
# simulate the call to try_window_condition
|
||||||
|
await try_window_condition(None)
|
||||||
|
assert entity.window_state == STATE_OFF
|
||||||
|
assert mock_heater_on.call_count == 2
|
||||||
|
assert mock_send_event.call_count == 2
|
||||||
|
mock_send_event.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
|
||||||
|
call.send_event(
|
||||||
|
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
any_order=False,
|
||||||
|
)
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"cycle_min": "Cycle duration (minutes)",
|
"cycle_min": "Cycle duration (minutes)",
|
||||||
"temp_min": "Minimal temperature allowed",
|
"temp_min": "Minimal temperature allowed",
|
||||||
"temp_max": "Maximal temperature allowed",
|
"temp_max": "Maximal temperature allowed",
|
||||||
|
"device_power": "Device power (kW)",
|
||||||
"use_window_feature": "Use window detection",
|
"use_window_feature": "Use window detection",
|
||||||
"use_motion_feature": "Use motion detection",
|
"use_motion_feature": "Use motion detection",
|
||||||
"use_power_feature": "Use power management",
|
"use_power_feature": "Use power management",
|
||||||
@@ -70,7 +71,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"power_sensor_entity_id": "Power sensor entity id",
|
"power_sensor_entity_id": "Power sensor entity id",
|
||||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||||
"device_power": "Device power (kW)",
|
|
||||||
"power_temp": "Temperature for Power shedding"
|
"power_temp": "Temperature for Power shedding"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -117,6 +117,7 @@
|
|||||||
"cycle_min": "Cycle duration (minutes)",
|
"cycle_min": "Cycle duration (minutes)",
|
||||||
"temp_min": "Minimal temperature allowed",
|
"temp_min": "Minimal temperature allowed",
|
||||||
"temp_max": "Maximal temperature allowed",
|
"temp_max": "Maximal temperature allowed",
|
||||||
|
"device_power": "Device power (kW)",
|
||||||
"use_window_feature": "Use window detection",
|
"use_window_feature": "Use window detection",
|
||||||
"use_motion_feature": "Use motion detection",
|
"use_motion_feature": "Use motion detection",
|
||||||
"use_power_feature": "Use power management",
|
"use_power_feature": "Use power management",
|
||||||
@@ -173,7 +174,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"power_sensor_entity_id": "Power sensor entity id",
|
"power_sensor_entity_id": "Power sensor entity id",
|
||||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||||
"device_power": "Device power (kW)",
|
|
||||||
"power_temp": "Temperature for Power shedding"
|
"power_temp": "Temperature for Power shedding"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"cycle_min": "Durée du cycle (minutes)",
|
"cycle_min": "Durée du cycle (minutes)",
|
||||||
"temp_min": "Température minimale permise",
|
"temp_min": "Température minimale permise",
|
||||||
"temp_max": "Température maximale permise",
|
"temp_max": "Température maximale permise",
|
||||||
|
"device_power": "Puissance de l'équipement",
|
||||||
"use_window_feature": "Avec détection des ouvertures",
|
"use_window_feature": "Avec détection des ouvertures",
|
||||||
"use_motion_feature": "Avec détection de mouvement",
|
"use_motion_feature": "Avec détection de mouvement",
|
||||||
"use_power_feature": "Avec gestion de la puissance",
|
"use_power_feature": "Avec gestion de la puissance",
|
||||||
@@ -69,7 +70,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"power_sensor_entity_id": "Capteur de puissance totale (entity id)",
|
"power_sensor_entity_id": "Capteur de puissance totale (entity id)",
|
||||||
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
|
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
|
||||||
"device_power": "Puissance de l'équipement",
|
|
||||||
"power_temp": "Température si délestaqe"
|
"power_temp": "Température si délestaqe"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -117,6 +117,7 @@
|
|||||||
"cycle_min": "Durée du cycle (minutes)",
|
"cycle_min": "Durée du cycle (minutes)",
|
||||||
"temp_min": "Température minimale permise",
|
"temp_min": "Température minimale permise",
|
||||||
"temp_max": "Température maximale permise",
|
"temp_max": "Température maximale permise",
|
||||||
|
"device_power": "Puissance de l'équipement",
|
||||||
"use_window_feature": "Avec détection des ouvertures",
|
"use_window_feature": "Avec détection des ouvertures",
|
||||||
"use_motion_feature": "Avec détection de mouvement",
|
"use_motion_feature": "Avec détection de mouvement",
|
||||||
"use_power_feature": "Avec gestion de la puissance",
|
"use_power_feature": "Avec gestion de la puissance",
|
||||||
@@ -173,7 +174,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"power_sensor_entity_id": "Capteur de puissance totale (entity id)",
|
"power_sensor_entity_id": "Capteur de puissance totale (entity id)",
|
||||||
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
|
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
|
||||||
"device_power": "Puissance de l'équipement",
|
|
||||||
"power_temp": "Température si délestaqe"
|
"power_temp": "Température si délestaqe"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user