With all testu developed and ok

This commit is contained in:
Jean-Marc Collin
2023-03-26 12:40:38 +02:00
parent 67d20dd083
commit 80fa977c15
10 changed files with 789 additions and 243 deletions

View File

@@ -3,7 +3,9 @@ default_config:
logger:
default: info
logs:
custom_components.versatile_thermostat: debug
custom_components.versatile_thermostat: info
custom_components.versatile_thermostat.underlyings: debug
custom_components.versatile_thermostat.climate: debug
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
debugpy:

View File

@@ -11,7 +11,6 @@ from homeassistant.core import (
HomeAssistant,
callback,
CoreState,
DOMAIN as HA_DOMAIN,
Event,
State,
)
@@ -73,8 +72,6 @@ from homeassistant.const import (
STATE_OFF,
STATE_ON,
EVENT_HOMEASSISTANT_START,
ATTR_ENTITY_ID,
SERVICE_TURN_ON,
STATE_HOME,
STATE_NOT_HOME,
)
@@ -143,6 +140,9 @@ from .open_window_algorithm import WindowOpenDetectionAlgorithm
_LOGGER = logging.getLogger(__name__)
# TODO remove this
_LOGGER.setLevel(logging.DEBUG)
async def async_setup_entry(
hass: HomeAssistant,
@@ -259,7 +259,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._thermostat_type = None
self._is_over_climate = False
# TODO should be delegated to underlying climate
self._heater_entity_id = None
# self._heater_entity_id = None
# self._climate_entity_id = None
self._underlying_climate = None
@@ -311,10 +311,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
else:
_LOGGER.debug("value %s not found in Entry", value)
# Stop eventual cycle running
if self._async_cancel_cycle is not None:
self._async_cancel_cycle()
self._async_cancel_cycle = None
if self._window_call_cancel is not None:
self._window_call_cancel()
self._window_call_cancel = None
@@ -522,10 +518,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._total_energy = 0
_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",
self,
self.unique_id,
self._heater_entity_id,
)
async def async_added_to_hass(self):
@@ -632,9 +627,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
def async_remove_thermostat(self):
"""Called when the thermostat will be removed"""
_LOGGER.info("%s - Removing thermostat", self)
if self._async_cancel_cycle:
self._async_cancel_cycle()
self._async_cancel_cycle = None
for under in self._underlyings:
under.remove_entity()
async def async_startup(self):
"""Triggered on startup, used to get old state and set internal states accordingly"""
@@ -687,22 +681,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self,
)
if self._is_over_climate:
climate_state = self.hass.states.get(self._climate_entity_id)
if climate_state and climate_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._hvac_mode = climate_state.state
need_write_state = True
else:
switch_state = self.hass.states.get(self._heater_entity_id)
if switch_state and switch_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self.hass.create_task(self._check_switch_initial_state())
if self._pmax_on:
# try to acquire current power and power max
current_power_state = self.hass.states.get(self._power_sensor_entity_id)
@@ -787,6 +765,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._prop_algorithm.calculate(
self._target_temp, self._cur_temp, self._cur_ext_temp
)
self.hass.create_task(self._check_switch_initial_state())
self.hass.create_task(self._async_control_heating())
await self.get_my_previous_state()
@@ -958,15 +938,20 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
return self._unit
@property
def hvac_mode(self):
def hvac_mode(self) -> HVACMode | None:
"""Return current operation."""
if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.hvac_mode
if self._is_over_climate:
# if one not OFF -> return it
# else OFF
for under in self._underlyings:
if (action := under.hvac_mode) not in [HVACMode.OFF]:
return action
return HVACMode.OFF
return self._hvac_mode
@property
def hvac_action(self):
def hvac_action(self) -> HVACAction | None:
"""Return the current running hvac operation if supported.
Need to be one of CURRENT_HVAC_*.
@@ -1545,12 +1530,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
async def _check_switch_initial_state(self):
"""Prevent the device from keep running if HVAC_MODE_OFF."""
_LOGGER.debug("%s - Calling _check_switch_initial_state", self)
if self._hvac_mode == HVACMode.OFF and self._is_device_active:
_LOGGER.warning(
"The climate mode is OFF, but the switch device is ON. Turning off device %s",
self._heater_entity_id,
)
await self._async_underlying_entity_turn_off()
if self.is_over_climate:
return
for under in self._underlyings:
await under.check_initial_state(self._hvac_mode)
@callback
def _async_switch_changed(self, event):
@@ -1838,10 +1821,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
async def _async_heater_turn_on(self):
"""Turn heater toggleable device on."""
data = {ATTR_ENTITY_ID: self._heater_entity_id}
await self.hass.services.async_call(
HA_DOMAIN, SERVICE_TURN_ON, data, context=self._context
)
# TODO should be delegated
# data = {ATTR_ENTITY_ID: self._heater_entity_id}
# await self.hass.services.async_call(
# HA_DOMAIN, SERVICE_TURN_ON, data, context=self._context
# )
for under in self._underlyings:
await under.turn_on()
async def _async_underlying_entity_turn_off(self):
"""Turn heater toggleable device off."""
@@ -2240,123 +2226,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
return
if not self._is_over_climate:
on_time_sec: int = self._prop_algorithm.on_time_sec
off_time_sec: int = self._prop_algorithm.off_time_sec
_LOGGER.debug(
"%s - Checking new cycle. on_time_sec=%.0f, off_time_sec=%.0f, security_state=%s, preset_mode=%s",
self,
on_time_sec,
off_time_sec,
self._security_state,
self._attr_preset_mode,
)
# Cancel eventual previous cycle if any
if self._async_cancel_cycle is not None:
if force:
_LOGGER.debug("%s - we force a new cycle", self)
self._async_cancel_cycle()
self._async_cancel_cycle = None
else:
_LOGGER.debug(
"%s - A previous cycle is alredy running and no force -> waits for its end",
self,
)
self._should_relaunch_control_heating = True
_LOGGER.debug("%s - End of cycle (2)", self)
return
if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0:
async def _turn_on_off_later(
on: bool, # pylint: disable=invalid-name
time,
heater_action,
next_cycle_action,
):
if self._async_cancel_cycle:
self._async_cancel_cycle()
self._async_cancel_cycle = None
_LOGGER.debug("%s - Stopping cycle during calculation", self)
if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
if self._is_device_active:
await self._async_underlying_entity_turn_off()
return
if on:
if await self.check_overpowering():
_LOGGER.debug("%s - End of cycle (3)", self)
return
# Security mode could have change the on_time percent
await self.check_security()
time = self._prop_algorithm.on_time_sec
action_label = "start" if on else "stop"
if self._should_relaunch_control_heating:
_LOGGER.debug(
"Don't %s cause a cycle have to be relaunch", action_label
)
self._should_relaunch_control_heating = False
self.hass.create_task(self._async_control_heating())
# await self._async_control_heating()
_LOGGER.debug("%s - End of cycle (3)", self)
return
if time > 0:
_LOGGER.info(
"%s - %s heating for %d min %d sec",
self,
action_label,
time // 60,
time % 60,
)
await heater_action()
else:
_LOGGER.debug(
"%s - No action on heater cause duration is 0", self
)
self._async_cancel_cycle = async_call_later(
self.hass,
time,
next_cycle_action,
)
async def _turn_on_later(_):
await _turn_on_off_later(
on=True,
time=self._prop_algorithm.on_time_sec,
heater_action=self._async_heater_turn_on,
next_cycle_action=_turn_off_later,
)
self.update_custom_attributes()
async def _turn_off_later(_):
await _turn_on_off_later(
on=False,
time=self._prop_algorithm.off_time_sec,
heater_action=self._async_underlying_entity_turn_off,
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)
elif self._is_device_active:
_LOGGER.info(
"%s - stop heating (2) for %d min %d sec",
self,
off_time_sec // 60,
off_time_sec % 60,
for under in self._underlyings:
await under.start_cycle(
self._hvac_mode,
self._prop_algorithm.on_time_sec,
self._prop_algorithm.off_time_sec,
force,
)
await self._async_underlying_entity_turn_off()
else:
_LOGGER.debug("%s - nothing to do", self)
self.update_custom_attributes()

View File

@@ -1,4 +1,6 @@
""" Some common resources """
import asyncio
import logging
from unittest.mock import patch, MagicMock
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
@@ -74,6 +76,8 @@ FULL_4SWITCH_CONFIG = (
| MOCK_ADVANCED_CONFIG
)
_LOGGER = logging.getLogger(__name__)
class MockClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode"""
@@ -195,8 +199,16 @@ def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
return None
async def send_temperature_change_event(entity: VersatileThermostat, new_temp, date):
async def send_temperature_change_event(
entity: VersatileThermostat, new_temp, date, sleep=True
):
"""Sending a new temperature event simulating a change on temperature sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_temp=%.2f date=%s on %s",
new_temp,
date,
entity,
)
temp_event = Event(
EVENT_STATE_CHANGED,
{
@@ -208,13 +220,21 @@ async def send_temperature_change_event(entity: VersatileThermostat, new_temp, d
)
},
)
return await entity._async_temperature_changed(temp_event)
await entity._async_temperature_changed(temp_event)
if sleep:
await asyncio.sleep(0.1)
async def send_ext_temperature_change_event(
entity: VersatileThermostat, new_temp, date
entity: VersatileThermostat, new_temp, date, sleep=True
):
"""Sending a new external temperature event simulating a change on temperature sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_temp=%.2f date=%s on %s",
new_temp,
date,
entity,
)
temp_event = Event(
EVENT_STATE_CHANGED,
{
@@ -226,11 +246,21 @@ async def send_ext_temperature_change_event(
)
},
)
return await entity._async_ext_temperature_changed(temp_event)
await entity._async_ext_temperature_changed(temp_event)
if sleep:
await asyncio.sleep(0.1)
async def send_power_change_event(entity: VersatileThermostat, new_power, date):
async def send_power_change_event(
entity: VersatileThermostat, new_power, date, sleep=True
):
"""Sending a new power event simulating a change on power sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_power=%.2f date=%s on %s",
new_power,
date,
entity,
)
power_event = Event(
EVENT_STATE_CHANGED,
{
@@ -242,11 +272,21 @@ async def send_power_change_event(entity: VersatileThermostat, new_power, date):
)
},
)
return await entity._async_power_changed(power_event)
await entity._async_power_changed(power_event)
if sleep:
await asyncio.sleep(0.1)
async def send_max_power_change_event(entity: VersatileThermostat, new_power_max, date):
async def send_max_power_change_event(
entity: VersatileThermostat, new_power_max, date, sleep=True
):
"""Sending a new power max event simulating a change on power max sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_power_max=%.2f date=%s on %s",
new_power_max,
date,
entity,
)
power_event = Event(
EVENT_STATE_CHANGED,
{
@@ -258,13 +298,22 @@ async def send_max_power_change_event(entity: VersatileThermostat, new_power_max
)
},
)
return await entity._async_max_power_changed(power_event)
await entity._async_max_power_changed(power_event)
if sleep:
await asyncio.sleep(0.1)
async def send_window_change_event(
entity: VersatileThermostat, new_state: bool, old_state: bool, date
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
):
"""Sending a new window event simulating a change on the window state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
new_state,
old_state,
date,
entity,
)
window_event = Event(
EVENT_STATE_CHANGED,
{
@@ -283,13 +332,22 @@ async def send_window_change_event(
},
)
ret = await entity._async_windows_changed(window_event)
if sleep:
await asyncio.sleep(0.1)
return ret
async def send_motion_change_event(
entity: VersatileThermostat, new_state: bool, old_state: bool, date
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
):
"""Sending a new motion event simulating a change on the window state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
new_state,
old_state,
date,
entity,
)
motion_event = Event(
EVENT_STATE_CHANGED,
{
@@ -308,13 +366,22 @@ async def send_motion_change_event(
},
)
ret = await entity._async_motion_changed(motion_event)
if sleep:
await asyncio.sleep(0.1)
return ret
async def send_presence_change_event(
entity: VersatileThermostat, new_state: bool, old_state: bool, date
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
):
"""Sending a new presence event simulating a change on the window state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
new_state,
old_state,
date,
entity,
)
presence_event = Event(
EVENT_STATE_CHANGED,
{
@@ -333,6 +400,8 @@ async def send_presence_change_event(
},
)
ret = await entity._async_presence_changed(presence_event)
if sleep:
await asyncio.sleep(0.1)
return ret
@@ -349,8 +418,18 @@ async def send_climate_change_event(
new_hvac_action: HVACAction,
old_hvac_action: HVACAction,
date,
sleep=True,
):
"""Sending a new climate event simulating a change on the underlying climate state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_hvac_mode=%s old_hvac_mode=%s new_hvac_action=%s old_hvac_action=%s date=%s on %s",
new_hvac_mode,
old_hvac_mode,
new_hvac_action,
old_hvac_action,
date,
entity,
)
climate_event = Event(
EVENT_STATE_CHANGED,
{
@@ -371,4 +450,14 @@ async def send_climate_change_event(
},
)
ret = await entity._async_climate_changed(climate_event)
if sleep:
await asyncio.sleep(0.1)
return ret
def cancel_switchs_cycles(entity: VersatileThermostat):
"""This method will cancel all running cycle on all underlying switch entity"""
if entity._is_over_climate:
return
for under in entity._underlyings:
under._cancel_cycle()

View File

@@ -241,17 +241,19 @@ async def test_bug_66(
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.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",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
await send_temperature_change_event(entity, 15, now)
try_window_condition = await send_window_change_event(entity, True, False, now)
try_window_condition = await send_window_change_event(
entity, True, False, now, False
)
# simulate the call to try_window_condition
await try_window_condition(None)
@@ -267,13 +269,13 @@ async def test_bug_66(
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
event_timestamp = now + timedelta(minutes=1)
@@ -290,13 +292,13 @@ async def test_bug_66(
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.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",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
try_window_condition = await send_window_change_event(
@@ -313,13 +315,13 @@ async def test_bug_66(
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.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",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
event_timestamp = now + timedelta(minutes=2)

View File

@@ -0,0 +1,337 @@
""" Test the Multiple switch management """
import asyncio
from unittest.mock import patch, call, ANY
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from datetime import datetime, timedelta
import logging
logging.getLogger().setLevel(logging.DEBUG)
async def test_one_switch_cycle(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
):
"""Test that when multiple switch are configured the activation is distributed"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4SwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4SwitchMockName",
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: 8,
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: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch1",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname"
)
assert entity
assert entity.is_over_climate is False
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
):
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.window_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Checks that all heaters are off
with patch(
"homeassistant.core.StateMachine.is_state", return_value=False
) as mock_is_state:
assert entity._is_device_active is False
# Should be call for the Switch
assert mock_is_state.call_count == 1
# Set temperature to a low level
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
) as mock_device_active, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
return_value=None,
) as mock_call_later:
await send_ext_temperature_change_event(entity, 5, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The first heater should be on but because call_later is mocked heater_on is not called
# assert mock_heater_on.call_count == 1
assert mock_heater_on.call_count == 0
# There is no check if active
assert mock_device_active.call_count == 0
# 4 calls dispatched along the cycle
assert mock_call_later.call_count == 1
mock_call_later.assert_has_calls(
[
call.call_later(hass, 0.0, ANY),
]
)
# Set a temperature at middle level
event_timestamp = now - timedelta(minutes=4)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
) as mock_device_active:
await send_temperature_change_event(entity, 18, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The first heater should be turned on but is already on but because above we mock call_later the heater is not on. But this time it will be really on
assert mock_heater_on.call_count == 1
# Set another temperature at middle level
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active:
await send_temperature_change_event(entity, 18.1, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The heater is already on cycle. So we wait that the cycle ends and no heater action is done
assert mock_heater_on.call_count == 0
assert entity.underlying_entity(0)._should_relaunch_control_heating is True
# Simulate the relaunch
await entity.underlying_entity(0)._turn_on_later(None)
# wait restart
await asyncio.sleep(0.1)
assert mock_heater_on.call_count == 1
assert entity.underlying_entity(0)._should_relaunch_control_heating is False
# Simulate the end of heater on cycle
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active:
await entity.underlying_entity(0)._turn_off_later(None)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
# The heater should be turned off this time
assert mock_heater_off.call_count == 1
assert entity.underlying_entity(0)._should_relaunch_control_heating is False
# Simulate the start of heater on cycle
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active:
await entity.underlying_entity(0)._turn_on_later(None)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 1
# The heater should be turned off this time
assert mock_heater_off.call_count == 0
assert entity.underlying_entity(0)._should_relaunch_control_heating is False
async def test_multiple_switchs(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
):
"""Test that when multiple switch are configured the activation is distributed"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4SwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4SwitchMockName",
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: 8,
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: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch1",
CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname"
)
assert entity
assert entity.is_over_climate is False
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
):
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.window_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Checks that all heaters are off
with patch(
"homeassistant.core.StateMachine.is_state", return_value=False
) as mock_is_state:
assert entity._is_device_active is False
# Should be call for all Switch
assert mock_is_state.call_count == 4
# Set temperature to a low level
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
) as mock_device_active, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
return_value=None,
) as mock_call_later:
await send_ext_temperature_change_event(entity, 5, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The first heater should be on but because call_later is mocked heater_on is not called
# assert mock_heater_on.call_count == 1
assert mock_heater_on.call_count == 0
# There is no check if active
assert mock_device_active.call_count == 0
# 4 calls dispatched along the cycle
assert mock_call_later.call_count == 4
mock_call_later.assert_has_calls(
[
call.call_later(hass, 0.0, ANY),
call.call_later(hass, 120.0, ANY),
call.call_later(hass, 240.0, ANY),
call.call_later(hass, 360.0, ANY),
]
)
# Set a temperature at middle level
event_timestamp = now - timedelta(minutes=4)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
) as mock_device_active:
await send_temperature_change_event(entity, 18, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The first heater should be turned on but is already on but because call_later is mocked, it is only turned on here
assert mock_heater_on.call_count == 1

View File

@@ -81,9 +81,9 @@ async def test_power_management_hvac_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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_max_power_change_event(entity, 149, datetime.now())
assert await entity.check_overpowering() is True
@@ -160,9 +160,9 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_max_power_change_event(entity, 149, datetime.now())
assert await entity.check_overpowering() is True
@@ -194,9 +194,9 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_power_change_event(entity, 48, datetime.now())
assert await entity.check_overpowering() is False
@@ -278,13 +278,13 @@ async def test_power_management_energy_over_switch(
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.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)
await send_temperature_change_event(entity, 15, datetime.now())
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
@@ -307,9 +307,9 @@ async def test_power_management_energy_over_switch(
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_temperature_change_event(entity, 18, datetime.now())
assert tpi_algo.on_percent == 0.3
@@ -329,9 +329,9 @@ async def test_power_management_energy_over_switch(
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_temperature_change_event(entity, 20, datetime.now())
assert tpi_algo.on_percent == 0.0

View File

@@ -87,7 +87,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = now - timedelta(minutes=6)
@@ -134,7 +134,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
await entity.async_set_preset_mode(PRESET_BOOST)
@@ -149,7 +149,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = datetime.now()
@@ -185,4 +185,5 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
any_order=True,
)
assert mock_heater_on.call_count == 0
# Heater is now on
assert mock_heater_on.call_count == 1

View File

@@ -1,4 +1,5 @@
""" Test the normal start of a Thermostat """
import asyncio
from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant
@@ -179,6 +180,8 @@ async def test_sensors_over_switch(
)
assert last_ext_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
cancel_switchs_cycles(entity)
async def test_sensors_over_climate(
hass: HomeAssistant,

View File

@@ -66,18 +66,16 @@ async def test_window_management_time_not_enough(
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition:
await send_temperature_change_event(entity, 15, datetime.now())
try_window_condition = await send_window_change_event(
entity, True, False, datetime.now()
)
# simulate the call to try_window_condition
await try_window_condition(None)
await send_window_change_event(entity, True, False, datetime.now())
# simulate the call to try_window_condition. No need due to 0 WINDOW_DELAY and sleep after event is sent
# await try_window_condition(None)
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 1
@@ -152,9 +150,9 @@ async def test_window_management_time_enough(
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
@@ -162,11 +160,7 @@ async def test_window_management_time_enough(
return_value=True,
):
await send_temperature_change_event(entity, 15, datetime.now())
try_window_condition = await send_window_change_event(
entity, True, False, datetime.now()
)
# simulate the call to try_window_condition
await try_window_condition(None)
await send_window_change_event(entity, True, False, datetime.now())
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
@@ -259,9 +253,9 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
return_value=True,
@@ -281,9 +275,9 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
return_value=True,
@@ -316,9 +310,9 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
new_callable=PropertyMock,
@@ -341,9 +335,9 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
new_callable=PropertyMock,
@@ -438,9 +432,9 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
return_value=True,
@@ -459,18 +453,32 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=3)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_temperature_change_event(entity, 18, event_timestamp, sleep=False)
# The heater turns on
assert mock_send_event.call_count == 2
# The heater turns off
mock_send_event.assert_has_calls(
[
call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
call.send_event(
EventType.WINDOW_AUTO_EVENT,
{
"type": "start",
"cause": "slope alert",
"curve_slope": -1.0,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count >= 1
assert entity.last_temperature_slope == -1
@@ -483,9 +491,9 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
return_value=False,
@@ -564,9 +572,9 @@ async def test_window_auto_no_on_percent(
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
return_value=True,
@@ -586,9 +594,9 @@ async def test_window_auto_no_on_percent(
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"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
return_value=True,

View File

@@ -3,8 +3,10 @@ import logging
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
from homeassistant.exceptions import ServiceNotFound
from homeassistant.backports.enum import StrEnum
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE
from homeassistant.components.climate import (
ClimateEntity,
DOMAIN as CLIMATE_DOMAIN,
@@ -15,14 +17,19 @@ from homeassistant.components.climate import (
SERVICE_SET_HUMIDITY,
SERVICE_SET_SWING_MODE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SERVICE_SET_TEMPERATURE,
)
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_call_later
from .const import UnknownEntity
_LOGGER = logging.getLogger(__name__)
# TODO remove this
_LOGGER.setLevel(logging.DEBUG)
class UnderlyingEntityType(StrEnum):
"""All underlying device type"""
@@ -88,23 +95,54 @@ class UnderlyingEntity:
async def turn_off(self):
"""Turn heater toggleable device off."""
_LOGGER.debug("%s - Stopping underlying switch %s", self, self._entity_id)
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
HA_DOMAIN,
SERVICE_TURN_OFF,
data, # TODO needed ? context=self._context
)
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
# This may fails if called after shutdown
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
HA_DOMAIN,
SERVICE_TURN_OFF,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
async def turn_on(self):
"""Turn heater toggleable device on."""
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
HA_DOMAIN,
SERVICE_TURN_ON,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
async def set_temperature(self, temperature, max_temp, min_temp):
"""Set the target temperature"""
return
async def remove_entity(self):
"""Remove the underlying entity"""
return
# override to be able to mock the call
def call_later(
self, hass: HomeAssistant, delay_sec: int, called_method
) -> CALLBACK_TYPE:
"""Call the method after a delay"""
return async_call_later(hass, delay_sec, called_method)
class UnderlyingSwitch(UnderlyingEntity):
"""Represent a underlying switch"""
_initialDelaySec: int
_on_time_sec: int
_off_time_sec: int
_hvac_mode: HVACMode
def __init__(
self,
@@ -122,6 +160,10 @@ class UnderlyingSwitch(UnderlyingEntity):
entity_id=switch_entity_id,
)
self._initial_delay_sec = initial_delay_sec
self._async_cancel_cycle = None
self._should_relaunch_control_heating = False
self._on_time_sec = 0
self._off_time_sec = 0
@property
def initial_delay_sec(self):
@@ -140,6 +182,185 @@ class UnderlyingSwitch(UnderlyingEntity):
"""If the toggleable device is currently active."""
return self._hass.states.is_state(self._entity_id, STATE_ON)
async def check_initial_state(self, hvac_mode: HVACMode):
"""Prevent the heater to be on but thermostat is off"""
if hvac_mode == HVACMode.OFF and self.is_device_active:
_LOGGER.warning(
"%s - The hvac mode is OFF, but the switch device is ON. Turning off device %s",
self,
self._entity_id,
)
await self.turn_off()
async def start_cycle(
self,
hvac_mode: HVACMode,
on_time_sec: int,
off_time_sec: int,
force=False,
):
"""Starting cycle for switch"""
_LOGGER.debug(
"%s - Starting new cycle hvac_mode=%s on_time_sec=%d off_time_sec=%d force=%s",
self,
hvac_mode,
on_time_sec,
off_time_sec,
force,
)
self._on_time_sec = on_time_sec
self._off_time_sec = off_time_sec
self._hvac_mode = hvac_mode
# Cancel eventual previous cycle if any
if self._async_cancel_cycle is not None:
if force:
_LOGGER.debug("%s - we force a new cycle", self)
await self._cancel_cycle()
else:
_LOGGER.debug(
"%s - A previous cycle is alredy running and no force -> waits for its end",
self,
)
self._should_relaunch_control_heating = True
_LOGGER.debug("%s - End of cycle (2)", self)
return
# If we should heat
if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0:
# Starts the cycle after the initial delay
self._async_cancel_cycle = self.call_later(
self._hass, self._initial_delay_sec, self._turn_on_later
)
_LOGGER.debug("%s - _async_cancel_cycle=%s", self, self._async_cancel_cycle)
# if we not heat but device is active
elif self.is_device_active:
_LOGGER.info(
"%s - stop heating (2) for %d min %d sec",
self,
off_time_sec // 60,
off_time_sec % 60,
)
await self.turn_off()
else:
_LOGGER.debug("%s - nothing to do", self)
async def _cancel_cycle(self):
"""Cancel the cycle"""
if self._async_cancel_cycle:
self._async_cancel_cycle()
self._async_cancel_cycle = None
_LOGGER.debug("%s - Stopping cycle during calculation", self)
async def _turn_on_later(self, _):
"""Turn the heater on after a delay"""
_LOGGER.debug(
"%s - calling turn_on_later hvac_mode=%s, should_relaunch_later=%s off_time_sec=%d",
self,
self._hvac_mode,
self._should_relaunch_control_heating,
self._on_time_sec,
)
await self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
if self.is_device_active:
await self.turn_off()
return
# TODO if await self.check_overpowering():
# _LOGGER.debug("%s - End of cycle (3)", self)
# return
# Security mode could have change the on_time percent
# TODO await self.check_security()
time = self._on_time_sec
action_label = "start"
if self._should_relaunch_control_heating:
_LOGGER.debug("Don't %s cause a cycle have to be relaunch", action_label)
self._should_relaunch_control_heating = False
# self.hass.create_task(self._async_control_heating())
await self.start_cycle(
self._hvac_mode, self._on_time_sec, self._off_time_sec
)
_LOGGER.debug("%s - End of cycle (3)", self)
return
if time > 0:
_LOGGER.info(
"%s - %s heating for %d min %d sec",
self,
action_label,
time // 60,
time % 60,
)
await self.turn_on()
else:
_LOGGER.debug("%s - No action on heater cause duration is 0", self)
self._async_cancel_cycle = self.call_later(
self._hass,
time,
self._turn_off_later,
)
async def _turn_off_later(self, _):
"""Turn the heater off and call the next cycle after the delay"""
_LOGGER.debug(
"%s - calling turn_off_later hvac_mode=%s, should_relaunch_later=%s off_time_sec=%d",
self,
self._hvac_mode,
self._should_relaunch_control_heating,
self._off_time_sec,
)
await self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
if self.is_device_active:
await self.turn_off()
return
action_label = "stop"
if self._should_relaunch_control_heating:
_LOGGER.debug("Don't %s cause a cycle have to be relaunch", action_label)
self._should_relaunch_control_heating = False
# self.hass.create_task(self._async_control_heating())
await self.start_cycle(
self._hvac_mode, self._on_time_sec, self._off_time_sec
)
_LOGGER.debug("%s - End of cycle (3)", self)
return
time = self._off_time_sec
if time > 0:
_LOGGER.info(
"%s - %s heating for %d min %d sec",
self,
action_label,
time // 60,
time % 60,
)
await self.turn_off()
else:
_LOGGER.debug("%s - No action on heater cause duration is 0", self)
self._async_cancel_cycle = self.call_later(
self._hass,
time,
self._turn_on_later,
)
# increment energy at the end of the cycle
# TODO self.incremente_energy()
async def remove_entity(self):
"""Remove the entity"""
await self._cancel_cycle()
class UnderlyingClimate(UnderlyingEntity):
"""Represent a underlying climate"""
@@ -286,3 +507,10 @@ class UnderlyingClimate(UnderlyingEntity):
if not self.is_initialized:
return None
return self._underlying_climate.hvac_action
@property
def hvac_mode(self) -> HVACMode:
"""Get the hvac mode of the underlying"""
if not self.is_initialized:
return None
return self._underlying_climate.hvac_mode