Compare commits
9 Commits
2.2.0
...
2.3.0.beta2
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b81a94d0f | |||
| 33590886c1 | |||
| 039b372a53 | |||
| a161540f10 | |||
| 8bbcafdf4a | |||
| 08d08e52de | |||
| 81b4f7e5f6 | |||
| 7a917c6ff7 | |||
| 20a9e2523e |
@@ -118,6 +118,17 @@ 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 %}
|
||||||
|
|
||||||
switch:
|
switch:
|
||||||
- platform: template
|
- platform: template
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ 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,
|
||||||
CoreState,
|
CoreState,
|
||||||
DOMAIN as HA_DOMAIN,
|
DOMAIN as HA_DOMAIN,
|
||||||
|
Event,
|
||||||
|
State,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.components.climate import ClimateEntity
|
from homeassistant.components.climate import ClimateEntity
|
||||||
@@ -132,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
|
||||||
@@ -197,6 +202,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# The list of VersatileThermostat entities
|
# The list of VersatileThermostat entities
|
||||||
# No more needed
|
# No more needed
|
||||||
# _registry: dict[str, object] = {}
|
# _registry: dict[str, object] = {}
|
||||||
|
_last_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."""
|
||||||
@@ -247,6 +260,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
self._attr_translation_key = "versatile_thermostat"
|
self._attr_translation_key = "versatile_thermostat"
|
||||||
|
|
||||||
|
self._total_energy = None
|
||||||
|
|
||||||
|
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):
|
||||||
@@ -343,8 +360,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._presets_away,
|
self._presets_away,
|
||||||
)
|
)
|
||||||
# Will be restored if possible
|
# Will be restored if possible
|
||||||
self._attr_preset_mode = None
|
self._attr_preset_mode = PRESET_NONE
|
||||||
self._saved_preset_mode = None
|
self._saved_preset_mode = PRESET_NONE
|
||||||
|
|
||||||
# Power management
|
# Power management
|
||||||
self._device_power = entry_infos.get(CONF_DEVICE_POWER)
|
self._device_power = entry_infos.get(CONF_DEVICE_POWER)
|
||||||
@@ -357,8 +374,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)
|
||||||
|
|
||||||
@@ -437,6 +452,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,
|
||||||
@@ -546,6 +563,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._async_cancel_cycle()
|
self._async_cancel_cycle()
|
||||||
self._async_cancel_cycle = None
|
self._async_cancel_cycle = None
|
||||||
|
|
||||||
|
def find_underlying_climate(self, climate_entity_id) -> ClimateEntity:
|
||||||
|
"""Find the underlying climate entity"""
|
||||||
|
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||||
|
for entity in component.entities:
|
||||||
|
if climate_entity_id == entity.entity_id:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
async def async_startup(self):
|
async def async_startup(self):
|
||||||
"""Triggered on startup, used to get old state and set internal states accordingly"""
|
"""Triggered on startup, used to get old state and set internal states accordingly"""
|
||||||
_LOGGER.debug("%s - Calling async_startup", self)
|
_LOGGER.debug("%s - Calling async_startup", self)
|
||||||
@@ -557,19 +582,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
# Get the underlying thermostat
|
# Get the underlying thermostat
|
||||||
if self._is_over_climate:
|
if self._is_over_climate:
|
||||||
component: EntityComponent[ClimateEntity] = self.hass.data[
|
self._underlying_climate = self.find_underlying_climate(
|
||||||
CLIMATE_DOMAIN
|
self._climate_entity_id
|
||||||
]
|
)
|
||||||
for entity in component.entities:
|
if self._underlying_climate:
|
||||||
if self._climate_entity_id == entity.entity_id:
|
_LOGGER.info(
|
||||||
_LOGGER.info(
|
"%s - The underlying climate entity: %s have been succesfully found",
|
||||||
"%s - The underlying climate entity: %s have been succesfully found",
|
self,
|
||||||
self,
|
self._underlying_climate,
|
||||||
entity,
|
)
|
||||||
)
|
else:
|
||||||
self._underlying_climate = entity
|
|
||||||
break
|
|
||||||
if self._underlying_climate is None:
|
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational",
|
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational",
|
||||||
self,
|
self,
|
||||||
@@ -771,6 +793,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:
|
||||||
@@ -924,7 +949,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return self.hass.states.is_state(self._heater_entity_id, STATE_ON)
|
return self._hass.states.is_state(self._heater_entity_id, STATE_ON)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self):
|
def current_temperature(self):
|
||||||
@@ -972,6 +997,36 @@ 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 self._is_over_climate:
|
||||||
|
return None
|
||||||
|
elif self._device_power:
|
||||||
|
return float(self._device_power * self._prop_algorithm.on_percent)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@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:
|
||||||
@@ -1071,6 +1126,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
if preset_mode == self._attr_preset_mode and not force:
|
if preset_mode == self._attr_preset_mode and not force:
|
||||||
# I don't think we need to call async_write_ha_state if we didn't change the state
|
# I don't think we need to call async_write_ha_state if we didn't change the state
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# In security mode don't change preset but memorise the new expected preset when security will be off
|
||||||
|
if preset_mode != PRESET_SECURITY and self._security_state:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s - is in security mode. Just memorise the new expected ", self
|
||||||
|
)
|
||||||
|
if preset_mode not in HIDDEN_PRESETS:
|
||||||
|
self._saved_preset_mode = preset_mode
|
||||||
|
return
|
||||||
|
|
||||||
old_preset_mode = self._attr_preset_mode
|
old_preset_mode = self._attr_preset_mode
|
||||||
if preset_mode == PRESET_NONE:
|
if preset_mode == PRESET_NONE:
|
||||||
self._attr_preset_mode = PRESET_NONE
|
self._attr_preset_mode = PRESET_NONE
|
||||||
@@ -1208,9 +1273,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
_LOGGER.info("%s - Change entry with the values: %s", self, config_entry.data)
|
_LOGGER.info("%s - Change entry with the values: %s", self, config_entry.data)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def _async_temperature_changed(self, event):
|
async def _async_temperature_changed(self, event: Event):
|
||||||
"""Handle temperature changes."""
|
"""Handle temperature of the temperature sensor changes."""
|
||||||
new_state = event.data.get("new_state")
|
new_state: State = event.data.get("new_state")
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s - Temperature changed. Event.new_state is %s",
|
"%s - Temperature changed. Event.new_state is %s",
|
||||||
self,
|
self,
|
||||||
@@ -1223,9 +1288,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self.recalculate()
|
self.recalculate()
|
||||||
await self._async_control_heating(force=False)
|
await self._async_control_heating(force=False)
|
||||||
|
|
||||||
async def _async_ext_temperature_changed(self, event):
|
async def _async_ext_temperature_changed(self, event: Event):
|
||||||
"""Handle external temperature changes."""
|
"""Handle external temperature opf the sensor changes."""
|
||||||
new_state = event.data.get("new_state")
|
new_state: State = event.data.get("new_state")
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s - external Temperature changed. Event.new_state is %s",
|
"%s - external Temperature changed. Event.new_state is %s",
|
||||||
self,
|
self,
|
||||||
@@ -1269,6 +1334,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)
|
||||||
@@ -1297,6 +1363,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
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):
|
||||||
@@ -1404,14 +1472,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
await self._async_control_heating(True)
|
await self._async_control_heating(True)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def _async_update_temp(self, state):
|
async def _async_update_temp(self, state: State):
|
||||||
"""Update thermostat with latest state from sensor."""
|
"""Update thermostat with latest state from sensor."""
|
||||||
try:
|
try:
|
||||||
cur_temp = float(state.state)
|
cur_temp = float(state.state)
|
||||||
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 = datetime.now()
|
self._last_temperature_mesure = (
|
||||||
|
state.last_changed if state.last_changed is not None else datetime.now()
|
||||||
|
)
|
||||||
# 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()
|
||||||
@@ -1420,14 +1490,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
_LOGGER.error("Unable to update temperature from sensor: %s", ex)
|
_LOGGER.error("Unable to update temperature from sensor: %s", ex)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def _async_update_ext_temp(self, state):
|
async def _async_update_ext_temp(self, state: State):
|
||||||
"""Update thermostat with latest state from sensor."""
|
"""Update thermostat with latest state from sensor."""
|
||||||
try:
|
try:
|
||||||
cur_ext_temp = float(state.state)
|
cur_ext_temp = float(state.state)
|
||||||
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 = datetime.now()
|
self._last_ext_temperature_mesure = (
|
||||||
|
state.last_changed if state.last_changed is not None else datetime.now()
|
||||||
|
)
|
||||||
# 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()
|
||||||
@@ -1648,7 +1720,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",
|
||||||
@@ -1657,6 +1742,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(
|
||||||
@@ -1707,10 +1793,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 = (now - self._last_temperature_mesure).total_seconds() / 60.0
|
delta_temp = (
|
||||||
|
now - self._last_temperature_mesure.replace(tzinfo=self._current_tz)
|
||||||
|
).total_seconds() / 60.0
|
||||||
delta_ext_temp = (
|
delta_ext_temp = (
|
||||||
now - self._last_ext_temperature_mesure
|
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
|
||||||
@@ -1730,6 +1818,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
>= self._security_min_on_percent
|
>= self._security_min_on_percent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s - checking security delta_temp=%.1f delta_ext_temp=%.1f mod_cond=%s temp_cond=%s climate_cond=%s switch_cond=%s",
|
||||||
|
self,
|
||||||
|
delta_temp,
|
||||||
|
delta_ext_temp,
|
||||||
|
mode_cond,
|
||||||
|
temp_cond,
|
||||||
|
climate_cond,
|
||||||
|
switch_cond,
|
||||||
|
)
|
||||||
|
|
||||||
ret = False
|
ret = False
|
||||||
if mode_cond and temp_cond and climate_cond:
|
if mode_cond and temp_cond and climate_cond:
|
||||||
if not self._security_state:
|
if not self._security_state:
|
||||||
@@ -1743,17 +1842,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
)
|
)
|
||||||
ret = True
|
ret = True
|
||||||
|
|
||||||
_LOGGER.debug(
|
|
||||||
"%s - checking security delta_temp=%.1f delta_ext_temp=%.1f mod_cond=%s temp_cond=%s climate_cond=%s switch_cond=%s",
|
|
||||||
self,
|
|
||||||
delta_temp,
|
|
||||||
delta_ext_temp,
|
|
||||||
mode_cond,
|
|
||||||
temp_cond,
|
|
||||||
climate_cond,
|
|
||||||
switch_cond,
|
|
||||||
)
|
|
||||||
|
|
||||||
if mode_cond and temp_cond and switch_cond:
|
if mode_cond and temp_cond and switch_cond:
|
||||||
if not self._security_state:
|
if not self._security_state:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
@@ -1771,8 +1859,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,
|
||||||
@@ -1794,8 +1886,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,
|
||||||
@@ -1824,8 +1920,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,
|
||||||
@@ -1939,7 +2039,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,
|
||||||
@@ -1953,6 +2052,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(
|
||||||
@@ -1961,6 +2061,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)
|
||||||
|
|
||||||
@@ -1993,6 +2096,11 @@ 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:
|
||||||
|
self._total_energy += self.mean_cycle_power * float(self._cycle_min) / 60.0
|
||||||
|
|
||||||
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"""
|
||||||
|
|
||||||
@@ -2025,11 +2133,21 @@ 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.replace(
|
||||||
"last_ext_temperature_datetime": self._last_ext_temperature_mesure.isoformat(),
|
tzinfo=self._current_tz
|
||||||
|
).isoformat(),
|
||||||
|
"last_ext_temperature_datetime": self._last_ext_temperature_mesure.replace(
|
||||||
|
tzinfo=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()
|
||||||
|
.replace(tzinfo=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[
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def validate_input(self, data: dict) -> dict[str]:
|
async def validate_input(self, data: dict) -> None:
|
||||||
"""Validate the user input allows us to connect.
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
Data has the keys from STEP_*_DATA_SCHEMA with values provided by the user.
|
Data has the keys from STEP_*_DATA_SCHEMA with values provided by the user.
|
||||||
|
|||||||
@@ -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,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",
|
||||||
@@ -85,6 +86,12 @@ class PropAlgorithm:
|
|||||||
def _calculate_internal(self):
|
def _calculate_internal(self):
|
||||||
"""Finish the calculation to get the on_percent in seconds"""
|
"""Finish the calculation to get the on_percent in seconds"""
|
||||||
|
|
||||||
|
# calculated on_time duration in seconds
|
||||||
|
if self._calculated_on_percent > 1:
|
||||||
|
self._calculated_on_percent = 1
|
||||||
|
if self._calculated_on_percent < 0:
|
||||||
|
self._calculated_on_percent = 0
|
||||||
|
|
||||||
if self._security:
|
if self._security:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Security is On using the default_on_percent %f",
|
"Security is On using the default_on_percent %f",
|
||||||
@@ -98,11 +105,6 @@ class PropAlgorithm:
|
|||||||
)
|
)
|
||||||
self._on_percent = self._calculated_on_percent
|
self._on_percent = self._calculated_on_percent
|
||||||
|
|
||||||
# calculated on_time duration in seconds
|
|
||||||
if self._on_percent > 1:
|
|
||||||
self._on_percent = 1
|
|
||||||
if self._on_percent < 0:
|
|
||||||
self._on_percent = 0
|
|
||||||
self._on_time_sec = self._on_percent * self._cycle_min * 60
|
self._on_time_sec = self._on_percent * self._cycle_min * 60
|
||||||
|
|
||||||
# Do not heat for less than xx sec
|
# Do not heat for less than xx sec
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
homeassistant
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# -r requirements_dev.txt
|
||||||
|
# aiodiscover
|
||||||
|
pytest-homeassistant-custom-component
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
""" To make this repo a module """
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
""" Some common resources """
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
|
||||||
|
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
|
from ..climate import VersatileThermostat
|
||||||
|
from ..const import *
|
||||||
|
|
||||||
|
from homeassistant.components.climate import (
|
||||||
|
ClimateEntity,
|
||||||
|
DOMAIN as CLIMATE_DOMAIN,
|
||||||
|
ATTR_PRESET_MODE,
|
||||||
|
HVACMode,
|
||||||
|
HVACAction,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
||||||
|
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
|
||||||
|
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
|
||||||
|
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
|
||||||
|
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
|
||||||
|
MOCK_PRESETS_CONFIG,
|
||||||
|
MOCK_WINDOW_CONFIG,
|
||||||
|
MOCK_MOTION_CONFIG,
|
||||||
|
MOCK_POWER_CONFIG,
|
||||||
|
MOCK_PRESENCE_CONFIG,
|
||||||
|
MOCK_ADVANCED_CONFIG,
|
||||||
|
# MOCK_DEFAULT_FEATURE_CONFIG,
|
||||||
|
PRESET_BOOST,
|
||||||
|
PRESET_COMFORT,
|
||||||
|
PRESET_NONE,
|
||||||
|
PRESET_ECO,
|
||||||
|
PRESET_ACTIVITY,
|
||||||
|
)
|
||||||
|
|
||||||
|
FULL_SWITCH_CONFIG = (
|
||||||
|
MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||||
|
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||||
|
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||||
|
| MOCK_PRESETS_CONFIG
|
||||||
|
| MOCK_WINDOW_CONFIG
|
||||||
|
| MOCK_MOTION_CONFIG
|
||||||
|
| MOCK_POWER_CONFIG
|
||||||
|
| MOCK_PRESENCE_CONFIG
|
||||||
|
| MOCK_ADVANCED_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
PARTIAL_CLIMATE_CONFIG = (
|
||||||
|
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||||
|
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||||
|
| MOCK_PRESETS_CONFIG
|
||||||
|
| MOCK_ADVANCED_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MockClimate(ClimateEntity):
|
||||||
|
"""A Mock Climate class used for Underlying climate mode"""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||||
|
"""Initialize the thermostat."""
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._hass = hass
|
||||||
|
self._attr_extra_state_attributes = {}
|
||||||
|
self._unique_id = unique_id
|
||||||
|
self._name = name
|
||||||
|
self._attr_hvac_action = HVACAction.OFF
|
||||||
|
self._attr_hvac_mode = HVACMode.OFF
|
||||||
|
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
|
||||||
|
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
|
|
||||||
|
|
||||||
|
async def create_thermostat(
|
||||||
|
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
||||||
|
) -> VersatileThermostat:
|
||||||
|
"""Creates and return a TPI Thermostat"""
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
):
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
def find_my_entity(entity_id) -> ClimateEntity:
|
||||||
|
"""Find my new entity"""
|
||||||
|
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||||
|
for entity in component.entities:
|
||||||
|
if entity.entity_id == entity_id:
|
||||||
|
return entity
|
||||||
|
|
||||||
|
entity = find_my_entity(entity_id)
|
||||||
|
|
||||||
|
return entity
|
||||||
|
|
||||||
|
|
||||||
|
async def send_temperature_change_event(entity: VersatileThermostat, new_temp, date):
|
||||||
|
"""Sending a new temperature event simulating a change on temperature sensor"""
|
||||||
|
temp_event = Event(
|
||||||
|
EVENT_STATE_CHANGED,
|
||||||
|
{
|
||||||
|
"new_state": State(
|
||||||
|
entity_id=entity.entity_id,
|
||||||
|
state=new_temp,
|
||||||
|
last_changed=date,
|
||||||
|
last_updated=date,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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, 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 not new_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)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""Global fixtures for integration_blueprint integration."""
|
||||||
|
# Fixtures allow you to replace functions with a Mock object. You can perform
|
||||||
|
# many options via the Mock to reflect a particular behavior from the original
|
||||||
|
# function that you want to see without going through the function's actual logic.
|
||||||
|
# Fixtures can either be passed into tests as parameters, or if autouse=True, they
|
||||||
|
# will automatically be used across all tests.
|
||||||
|
#
|
||||||
|
# Fixtures that are defined in conftest.py are available across all tests. You can also
|
||||||
|
# define fixtures within a particular test file to scope them locally.
|
||||||
|
#
|
||||||
|
# pytest_homeassistant_custom_component provides some fixtures that are provided by
|
||||||
|
# Home Assistant core. You can find those fixture definitions here:
|
||||||
|
# https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/blob/master/pytest_homeassistant_custom_component/common.py
|
||||||
|
#
|
||||||
|
# See here for more info: https://docs.pytest.org/en/latest/fixture.html (note that
|
||||||
|
# pytest includes fixtures OOB which you can use as defined on this page)
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, StateMachine
|
||||||
|
|
||||||
|
from custom_components.versatile_thermostat.config_flow import (
|
||||||
|
VersatileThermostatBaseConfigFlow,
|
||||||
|
)
|
||||||
|
|
||||||
|
from custom_components.versatile_thermostat.climate import (
|
||||||
|
VersatileThermostat,
|
||||||
|
)
|
||||||
|
|
||||||
|
pytest_plugins = "pytest_homeassistant_custom_component"
|
||||||
|
|
||||||
|
|
||||||
|
# This fixture enables loading custom integrations in all tests.
|
||||||
|
# Remove to enable selective use of this fixture
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def auto_enable_custom_integrations(enable_custom_integrations):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
# This fixture is used to prevent HomeAssistant from attempting to create and dismiss persistent
|
||||||
|
# notifications. These calls would fail without this fixture since the persistent_notification
|
||||||
|
# integration is never loaded during a test.
|
||||||
|
@pytest.fixture(name="skip_notifications", autouse=True)
|
||||||
|
def skip_notifications_fixture():
|
||||||
|
"""Skip notification calls."""
|
||||||
|
with patch("homeassistant.components.persistent_notification.async_create"), patch(
|
||||||
|
"homeassistant.components.persistent_notification.async_dismiss"
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
# This fixture is used to bypass the validate_input function in config_flow
|
||||||
|
# NOT USED Now (keeped for memory)
|
||||||
|
@pytest.fixture(name="skip_validate_input")
|
||||||
|
def skip_validate_input_fixture():
|
||||||
|
"""Skip the validate_input in config flow"""
|
||||||
|
with patch.object(VersatileThermostatBaseConfigFlow, "validate_input"):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="skip_hass_states_get")
|
||||||
|
def skip_hass_states_get_fixture():
|
||||||
|
"""Skip the get state in HomeAssistant"""
|
||||||
|
with patch.object(StateMachine, "get"):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="skip_hass_states_is_state")
|
||||||
|
def skip_hass_states_is_state_fixture():
|
||||||
|
"""Skip the is_state in HomeAssistant"""
|
||||||
|
with patch.object(StateMachine, "is_state", return_value=False):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="skip_send_event")
|
||||||
|
def skip_send_event_fixture():
|
||||||
|
"""Skip the send_event in VersatileThermostat"""
|
||||||
|
with patch.object(VersatileThermostat, "send_event"):
|
||||||
|
yield
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
from homeassistant.components.climate.const import (
|
||||||
|
PRESET_BOOST,
|
||||||
|
PRESET_COMFORT,
|
||||||
|
PRESET_ECO,
|
||||||
|
PRESET_NONE,
|
||||||
|
PRESET_ACTIVITY,
|
||||||
|
)
|
||||||
|
from custom_components.versatile_thermostat.const import (
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_HEATER,
|
||||||
|
CONF_THERMOSTAT_CLIMATE,
|
||||||
|
CONF_THERMOSTAT_SWITCH,
|
||||||
|
CONF_THERMOSTAT_TYPE,
|
||||||
|
CONF_TEMP_SENSOR,
|
||||||
|
CONF_EXTERNAL_TEMP_SENSOR,
|
||||||
|
CONF_CYCLE_MIN,
|
||||||
|
CONF_TEMP_MAX,
|
||||||
|
CONF_TEMP_MIN,
|
||||||
|
CONF_PROP_FUNCTION,
|
||||||
|
PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT,
|
||||||
|
CONF_TPI_COEF_EXT,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY,
|
||||||
|
CONF_SECURITY_DELAY_MIN,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT,
|
||||||
|
CONF_SECURITY_DEFAULT_ON_PERCENT,
|
||||||
|
CONF_USE_WINDOW_FEATURE,
|
||||||
|
CONF_USE_MOTION_FEATURE,
|
||||||
|
CONF_USE_POWER_FEATURE,
|
||||||
|
CONF_USE_PRESENCE_FEATURE,
|
||||||
|
CONF_WINDOW_SENSOR,
|
||||||
|
CONF_WINDOW_DELAY,
|
||||||
|
CONF_MOTION_SENSOR,
|
||||||
|
CONF_MOTION_DELAY,
|
||||||
|
CONF_MOTION_PRESET,
|
||||||
|
CONF_NO_MOTION_PRESET,
|
||||||
|
CONF_POWER_SENSOR,
|
||||||
|
CONF_MAX_POWER_SENSOR,
|
||||||
|
CONF_DEVICE_POWER,
|
||||||
|
CONF_PRESET_POWER,
|
||||||
|
CONF_PRESENCE_SENSOR,
|
||||||
|
PRESET_AWAY_SUFFIX,
|
||||||
|
CONF_CLIMATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
||||||
|
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,
|
||||||
|
CONF_USE_WINDOW_FEATURE: True,
|
||||||
|
CONF_USE_MOTION_FEATURE: True,
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: True,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
|
||||||
|
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,
|
||||||
|
# Keep default values which are False
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
||||||
|
CONF_HEATER: "switch.mock_switch",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.1,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
||||||
|
CONF_CLIMATE: "climate.mock_climate",
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_PRESETS_CONFIG = {
|
||||||
|
PRESET_ECO + "_temp": 16,
|
||||||
|
PRESET_COMFORT + "_temp": 17,
|
||||||
|
PRESET_BOOST + "_temp": 18,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_WINDOW_CONFIG = {
|
||||||
|
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
||||||
|
CONF_WINDOW_DELAY: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_MOTION_CONFIG = {
|
||||||
|
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
|
||||||
|
CONF_MOTION_DELAY: 10,
|
||||||
|
CONF_MOTION_PRESET: PRESET_COMFORT,
|
||||||
|
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_POWER_CONFIG = {
|
||||||
|
CONF_POWER_SENSOR: "sensor.power_sensor",
|
||||||
|
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
|
||||||
|
CONF_DEVICE_POWER: 1,
|
||||||
|
CONF_PRESET_POWER: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_PRESENCE_CONFIG = {
|
||||||
|
CONF_PRESENCE_SENSOR: "person.presence_sensor",
|
||||||
|
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
|
||||||
|
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
|
||||||
|
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_ADVANCED_CONFIG = {
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||||
|
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_DEFAULT_FEATURE_CONFIG = {
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
""" Test the Versatile Thermostat config flow """
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.config_entries import SOURCE_USER, ConfigEntry
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pytest_homeassistant_custom_component.common import MockConfigEntry, load_fixture
|
||||||
|
|
||||||
|
from custom_components.versatile_thermostat.const import DOMAIN
|
||||||
|
from custom_components.versatile_thermostat import VersatileThermostatAPI
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
||||||
|
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
|
||||||
|
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
|
||||||
|
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
|
||||||
|
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
|
||||||
|
MOCK_PRESETS_CONFIG,
|
||||||
|
MOCK_WINDOW_CONFIG,
|
||||||
|
MOCK_MOTION_CONFIG,
|
||||||
|
MOCK_POWER_CONFIG,
|
||||||
|
MOCK_PRESENCE_CONFIG,
|
||||||
|
MOCK_ADVANCED_CONFIG,
|
||||||
|
MOCK_DEFAULT_FEATURE_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_show_form(hass: HomeAssistant) -> None:
|
||||||
|
"""Test that the form is served with no input"""
|
||||||
|
# Init the API
|
||||||
|
# hass.data["custom_components"] = None
|
||||||
|
# loader.async_get_custom_components(hass)
|
||||||
|
# VersatileThermostatAPI(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == SOURCE_USER
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_config_flow_over_switch(hass, skip_hass_states_get):
|
||||||
|
"""Test the config flow with all thermostat_over_switch features"""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == SOURCE_USER
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "type"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "tpi"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "presets"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "window"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_WINDOW_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "motion"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_MOTION_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "power"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_POWER_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "presence"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_PRESENCE_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "advanced"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert (
|
||||||
|
result["data"]
|
||||||
|
== MOCK_TH_OVER_SWITCH_USER_CONFIG
|
||||||
|
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||||
|
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||||
|
| MOCK_PRESETS_CONFIG
|
||||||
|
| MOCK_WINDOW_CONFIG
|
||||||
|
| MOCK_MOTION_CONFIG
|
||||||
|
| MOCK_POWER_CONFIG
|
||||||
|
| MOCK_PRESENCE_CONFIG
|
||||||
|
| MOCK_ADVANCED_CONFIG
|
||||||
|
)
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].domain == DOMAIN
|
||||||
|
assert result["result"].version == 1
|
||||||
|
assert result["result"].title == "TheOverSwitchMockName"
|
||||||
|
assert isinstance(result["result"], ConfigEntry)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_config_flow_over_climate(hass, skip_hass_states_get):
|
||||||
|
"""Test the config flow with all thermostat_over_climate features and no additional features"""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == SOURCE_USER
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "type"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "presets"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
# assert result["step_id"] == "window"
|
||||||
|
# assert result["errors"] == {}
|
||||||
|
|
||||||
|
# result = await hass.config_entries.flow.async_configure(
|
||||||
|
# result["flow_id"], user_input=MOCK_WINDOW_CONFIG
|
||||||
|
# )
|
||||||
|
|
||||||
|
# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
# assert result["step_id"] == "motion"
|
||||||
|
# assert result["errors"] == {}
|
||||||
|
|
||||||
|
# result = await hass.config_entries.flow.async_configure(
|
||||||
|
# result["flow_id"], user_input=MOCK_MOTION_CONFIG
|
||||||
|
# )
|
||||||
|
|
||||||
|
# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
# assert result["step_id"] == "power"
|
||||||
|
# assert result["errors"] == {}
|
||||||
|
|
||||||
|
# result = await hass.config_entries.flow.async_configure(
|
||||||
|
# result["flow_id"], user_input=MOCK_POWER_CONFIG
|
||||||
|
# )
|
||||||
|
|
||||||
|
# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
# assert result["step_id"] == "presence"
|
||||||
|
# assert result["errors"] == {}
|
||||||
|
|
||||||
|
# result = await hass.config_entries.flow.async_configure(
|
||||||
|
# result["flow_id"], user_input=MOCK_PRESENCE_CONFIG
|
||||||
|
# )
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "advanced"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert (
|
||||||
|
result["data"]
|
||||||
|
== MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||||
|
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||||
|
| MOCK_PRESETS_CONFIG
|
||||||
|
| MOCK_ADVANCED_CONFIG
|
||||||
|
| MOCK_DEFAULT_FEATURE_CONFIG
|
||||||
|
)
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].domain == DOMAIN
|
||||||
|
assert result["result"].version == 1
|
||||||
|
assert result["result"].title == "TheOverClimateMockName"
|
||||||
|
assert isinstance(result["result"], ConfigEntry)
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
""" Test the Power management """
|
||||||
|
from unittest.mock import patch, call
|
||||||
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
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(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)
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
""" Test the Security featrure """
|
||||||
|
from unittest.mock import patch, call
|
||||||
|
|
||||||
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
|
||||||
|
from datetime import timedelta, datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||||
|
"""Test the security feature and https://github.com/jmcollin78/versatile_thermostat/issues/49:
|
||||||
|
1. creates a thermostat and check that security is off
|
||||||
|
2. activate security feature when date is expired
|
||||||
|
3. change the preset to boost
|
||||||
|
4. check that security is still on
|
||||||
|
5. resolve the date issue
|
||||||
|
6. check that security is off and preset is changed to boost
|
||||||
|
"""
|
||||||
|
|
||||||
|
tz = get_tz(hass)
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
"name": "TheOverSwitchMockName",
|
||||||
|
"thermostat_type": "thermostat_over_switch",
|
||||||
|
"temperature_sensor_entity_id": "sensor.mock_temp_sensor",
|
||||||
|
"external_temperature_sensor_entity_id": "sensor.mock_ext_temp_sensor",
|
||||||
|
"cycle_min": 5,
|
||||||
|
"temp_min": 15,
|
||||||
|
"temp_max": 30,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
"use_window_feature": False,
|
||||||
|
"use_motion_feature": False,
|
||||||
|
"use_power_feature": False,
|
||||||
|
"use_presence_feature": False,
|
||||||
|
"heater_entity_id": "switch.mock_switch",
|
||||||
|
"proportional_function": "tpi",
|
||||||
|
"tpi_coef_int": 0.3,
|
||||||
|
"tpi_coef_ext": 0.01,
|
||||||
|
"minimal_activation_delay": 30,
|
||||||
|
"security_delay_min": 5, # 5 minutes
|
||||||
|
"security_min_on_percent": 0.2,
|
||||||
|
"security_default_on_percent": 0.1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1. creates a thermostat and check that security is off
|
||||||
|
now: datetime = datetime.now()
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
assert entity._security_state is False
|
||||||
|
assert entity.preset_mode is not PRESET_SECURITY
|
||||||
|
assert entity.preset_modes == [
|
||||||
|
PRESET_NONE,
|
||||||
|
PRESET_ECO,
|
||||||
|
PRESET_COMFORT,
|
||||||
|
PRESET_BOOST,
|
||||||
|
]
|
||||||
|
assert entity._last_ext_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_ext_temperature_mesure - now).total_seconds() < 1
|
||||||
|
|
||||||
|
# set a preset
|
||||||
|
assert entity.preset_mode is PRESET_NONE
|
||||||
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
|
assert entity.preset_mode is PRESET_COMFORT
|
||||||
|
|
||||||
|
# Turn On the thermostat
|
||||||
|
assert entity.hvac_mode == HVACMode.OFF
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
|
||||||
|
# 2. activate security feature when date is expired
|
||||||
|
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:
|
||||||
|
event_timestamp = now - timedelta(minutes=6)
|
||||||
|
|
||||||
|
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||||
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||||
|
assert entity._security_state is True
|
||||||
|
assert entity.preset_mode == PRESET_SECURITY
|
||||||
|
assert entity._saved_preset_mode == PRESET_COMFORT
|
||||||
|
assert entity._prop_algorithm.on_percent == 0.1
|
||||||
|
assert entity._prop_algorithm.calculated_on_percent == 0.9
|
||||||
|
|
||||||
|
assert mock_send_event.call_count == 3
|
||||||
|
mock_send_event.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_SECURITY}),
|
||||||
|
call.send_event(
|
||||||
|
EventType.TEMPERATURE_EVENT,
|
||||||
|
{
|
||||||
|
"last_temperature_mesure": event_timestamp.replace(
|
||||||
|
tzinfo=tz
|
||||||
|
).isoformat(),
|
||||||
|
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.replace(
|
||||||
|
tzinfo=tz
|
||||||
|
).isoformat(),
|
||||||
|
"current_temp": 15,
|
||||||
|
"current_ext_temp": None,
|
||||||
|
"target_temp": 18,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
call.send_event(
|
||||||
|
EventType.SECURITY_EVENT,
|
||||||
|
{
|
||||||
|
"type": "start",
|
||||||
|
"last_temperature_mesure": event_timestamp.replace(
|
||||||
|
tzinfo=tz
|
||||||
|
).isoformat(),
|
||||||
|
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.replace(
|
||||||
|
tzinfo=tz
|
||||||
|
).isoformat(),
|
||||||
|
"current_temp": 15,
|
||||||
|
"current_ext_temp": None,
|
||||||
|
"target_temp": 18,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
any_order=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_heater_on.call_count == 1
|
||||||
|
|
||||||
|
# 3. Change the preset to Boost (we should stay in SECURITY)
|
||||||
|
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:
|
||||||
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
|
|
||||||
|
# 4. check that security is still on
|
||||||
|
assert entity._security_state is True
|
||||||
|
assert entity._prop_algorithm.on_percent == 0.1
|
||||||
|
assert entity._prop_algorithm.calculated_on_percent == 0.9
|
||||||
|
assert entity._saved_preset_mode == PRESET_BOOST
|
||||||
|
assert entity.preset_mode is PRESET_SECURITY
|
||||||
|
|
||||||
|
# 5. resolve the datetime issue
|
||||||
|
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:
|
||||||
|
event_timestamp = datetime.now()
|
||||||
|
|
||||||
|
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||||
|
await send_temperature_change_event(entity, 15.2, event_timestamp)
|
||||||
|
|
||||||
|
assert entity._security_state is False
|
||||||
|
assert entity.preset_mode == PRESET_BOOST
|
||||||
|
assert entity._saved_preset_mode == PRESET_BOOST
|
||||||
|
assert entity._prop_algorithm.on_percent == 1.0
|
||||||
|
assert entity._prop_algorithm.calculated_on_percent == 1.0
|
||||||
|
|
||||||
|
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.SECURITY_EVENT,
|
||||||
|
{
|
||||||
|
"type": "end",
|
||||||
|
"last_temperature_mesure": event_timestamp.replace(
|
||||||
|
tzinfo=tz
|
||||||
|
).isoformat(),
|
||||||
|
"last_ext_temperature_mesure": entity._last_ext_temperature_mesure.replace(
|
||||||
|
tzinfo=tz
|
||||||
|
).isoformat(),
|
||||||
|
"current_temp": 15.2,
|
||||||
|
"current_ext_temp": None,
|
||||||
|
"target_temp": 19,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
any_order=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_heater_on.call_count == 0
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
""" Test the normal start of a Thermostat """
|
||||||
|
from unittest.mock import patch, call
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.components.climate import HVACAction, HVACMode
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||||
|
|
||||||
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
|
from ..climate import VersatileThermostat
|
||||||
|
|
||||||
|
from .commons import *
|
||||||
|
|
||||||
|
|
||||||
|
async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_state):
|
||||||
|
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data=FULL_SWITCH_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
) as mock_send_event:
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
def find_my_entity(entity_id) -> ClimateEntity:
|
||||||
|
"""Find my new entity"""
|
||||||
|
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||||
|
for entity in component.entities:
|
||||||
|
if entity.entity_id == entity_id:
|
||||||
|
return entity
|
||||||
|
|
||||||
|
entity: VersatileThermostat = find_my_entity("climate.theoverswitchmockname")
|
||||||
|
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
assert entity.name == "TheOverSwitchMockName"
|
||||||
|
assert entity._is_over_climate is False
|
||||||
|
assert entity.hvac_action is HVACAction.OFF
|
||||||
|
assert entity.hvac_mode is HVACMode.OFF
|
||||||
|
assert entity.target_temperature == entity.min_temp
|
||||||
|
assert entity.preset_modes == [
|
||||||
|
PRESET_NONE,
|
||||||
|
PRESET_ECO,
|
||||||
|
PRESET_COMFORT,
|
||||||
|
PRESET_BOOST,
|
||||||
|
PRESET_ACTIVITY,
|
||||||
|
]
|
||||||
|
assert entity.preset_mode is PRESET_NONE
|
||||||
|
assert entity._security_state is False
|
||||||
|
assert entity._window_state is None
|
||||||
|
assert entity._motion_state is None
|
||||||
|
assert entity._presence_state is None
|
||||||
|
assert entity._prop_algorithm is not None
|
||||||
|
|
||||||
|
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||||
|
assert mock_send_event.call_count == 2
|
||||||
|
|
||||||
|
mock_send_event.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||||
|
call.send_event(
|
||||||
|
EventType.HVAC_MODE_EVENT,
|
||||||
|
{"hvac_mode": HVACMode.OFF},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_state):
|
||||||
|
"""Test the normal full start of a thermostat in thermostat_over_climate type"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverClimateMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data=PARTIAL_CLIMATE_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
) as mock_send_event, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||||
|
return_value=fake_underlying_climate,
|
||||||
|
) as mock_find_climate:
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
def find_my_entity(entity_id) -> ClimateEntity:
|
||||||
|
"""Find my new entity"""
|
||||||
|
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||||
|
for entity in component.entities:
|
||||||
|
if entity.entity_id == entity_id:
|
||||||
|
return entity
|
||||||
|
|
||||||
|
entity = find_my_entity("climate.theoverclimatemockname")
|
||||||
|
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
assert entity.name == "TheOverClimateMockName"
|
||||||
|
assert entity._is_over_climate is True
|
||||||
|
assert entity.hvac_action is HVACAction.OFF
|
||||||
|
assert entity.hvac_mode is HVACMode.OFF
|
||||||
|
assert entity.target_temperature == entity.min_temp
|
||||||
|
assert entity.preset_modes == [
|
||||||
|
PRESET_NONE,
|
||||||
|
PRESET_ECO,
|
||||||
|
PRESET_COMFORT,
|
||||||
|
PRESET_BOOST,
|
||||||
|
]
|
||||||
|
assert entity.preset_mode is PRESET_NONE
|
||||||
|
assert entity._security_state is False
|
||||||
|
assert entity._window_state is None
|
||||||
|
assert entity._motion_state is None
|
||||||
|
assert entity._presence_state is None
|
||||||
|
|
||||||
|
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||||
|
assert mock_send_event.call_count == 2
|
||||||
|
mock_send_event.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||||
|
call.send_event(
|
||||||
|
EventType.HVAC_MODE_EVENT,
|
||||||
|
{"hvac_mode": HVACMode.OFF},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_find_climate.call_count == 1
|
||||||
|
assert mock_find_climate.mock_calls[0] == call("climate.mock_climate")
|
||||||
|
mock_find_climate.assert_has_calls(
|
||||||
|
[call.find_underlying_entity("climate.mock_climate")]
|
||||||
|
)
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
""" Test the TPI algorithm """
|
||||||
|
|
||||||
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
|
||||||
|
|
||||||
|
async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
|
||||||
|
"""Test the TPI calculation"""
|
||||||
|
|
||||||
|
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,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
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_DEVICE_POWER: 100,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
tpi_algo = entity._prop_algorithm
|
||||||
|
assert tpi_algo
|
||||||
|
|
||||||
|
tpi_algo.calculate(15, 10, 7)
|
||||||
|
assert tpi_algo.on_percent == 1
|
||||||
|
assert tpi_algo.calculated_on_percent == 1
|
||||||
|
assert tpi_algo.on_time_sec == 300
|
||||||
|
assert tpi_algo.off_time_sec == 0
|
||||||
|
assert entity.mean_cycle_power is None # no device power configured
|
||||||
|
|
||||||
|
tpi_algo.calculate(15, 14, 5)
|
||||||
|
assert tpi_algo.on_percent == 0.4
|
||||||
|
assert tpi_algo.calculated_on_percent == 0.4
|
||||||
|
assert tpi_algo.on_time_sec == 120
|
||||||
|
assert tpi_algo.off_time_sec == 180
|
||||||
|
|
||||||
|
tpi_algo.set_security(0.1)
|
||||||
|
tpi_algo.calculate(15, 14, 5)
|
||||||
|
assert tpi_algo.on_percent == 0.1
|
||||||
|
assert tpi_algo.calculated_on_percent == 0.4
|
||||||
|
assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30)
|
||||||
|
assert tpi_algo.off_time_sec == 270
|
||||||
|
|
||||||
|
tpi_algo.unset_security()
|
||||||
|
tpi_algo.calculate(15, 14, 5)
|
||||||
|
assert tpi_algo.on_percent == 0.4
|
||||||
|
assert tpi_algo.calculated_on_percent == 0.4
|
||||||
|
assert tpi_algo.on_time_sec == 120
|
||||||
|
assert tpi_algo.off_time_sec == 180
|
||||||
|
|
||||||
|
# Test minimal activation delay
|
||||||
|
tpi_algo.calculate(15, 14.7, 15)
|
||||||
|
assert tpi_algo.on_percent == 0.09
|
||||||
|
assert tpi_algo.calculated_on_percent == 0.09
|
||||||
|
assert tpi_algo.on_time_sec == 0
|
||||||
|
assert tpi_algo.off_time_sec == 300
|
||||||
|
|
||||||
|
tpi_algo.set_security(0.09)
|
||||||
|
tpi_algo.calculate(15, 14.7, 15)
|
||||||
|
assert tpi_algo.on_percent == 0.09
|
||||||
|
assert tpi_algo.calculated_on_percent == 0.09
|
||||||
|
assert tpi_algo.on_time_sec == 0
|
||||||
|
assert tpi_algo.off_time_sec == 300
|
||||||
@@ -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, 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, 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, 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, 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
|
||||||
Reference in New Issue
Block a user