Compare commits
1 Commits
3.1.0.alph
...
3.1.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63cf77abc9 |
@@ -11,6 +11,10 @@ from datetime import datetime
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# To filter bad values
|
||||||
|
MIN_DELTA_T_SEC = 10 # two temp mesure should be > 10 sec
|
||||||
|
MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point
|
||||||
|
|
||||||
|
|
||||||
class WindowOpenDetectionAlgorithm:
|
class WindowOpenDetectionAlgorithm:
|
||||||
"""The class that implements the algorithm listed above"""
|
"""The class that implements the algorithm listed above"""
|
||||||
@@ -45,17 +49,28 @@ class WindowOpenDetectionAlgorithm:
|
|||||||
self._last_slope,
|
self._last_slope,
|
||||||
self._last_temperature,
|
self._last_temperature,
|
||||||
)
|
)
|
||||||
delta_t = float((datetime_measure - self._last_datetime).total_seconds() / 60.0)
|
lspe = self._last_slope
|
||||||
if delta_t <= 0:
|
|
||||||
|
delta_t_sec = float((datetime_measure - self._last_datetime).total_seconds())
|
||||||
|
delta_t = delta_t_sec / 60.0
|
||||||
|
if delta_t_sec <= MIN_DELTA_T_SEC:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Delta t is 0 or < 0 which should be not possible. We stop here the open window detection algorithm"
|
"Delta t is %d < %d which should be not possible. We don't consider this value",
|
||||||
|
delta_t_sec,
|
||||||
|
MIN_DELTA_T_SEC,
|
||||||
)
|
)
|
||||||
return None
|
return lspe
|
||||||
|
|
||||||
delta_temp = float(temperature - self._last_temperature)
|
delta_temp = float(temperature - self._last_temperature)
|
||||||
new_slope = delta_temp / delta_t
|
new_slope = delta_temp / delta_t
|
||||||
|
if new_slope > MAX_SLOPE_VALUE or new_slope < -MAX_SLOPE_VALUE:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"New_slope is abs(%.2f) > %.2f which should be not possible. We don't consider this value",
|
||||||
|
new_slope,
|
||||||
|
MAX_SLOPE_VALUE,
|
||||||
|
)
|
||||||
|
return lspe
|
||||||
|
|
||||||
lspe = self._last_slope
|
|
||||||
if self._last_slope is None:
|
if self._last_slope is None:
|
||||||
self._last_slope = new_slope
|
self._last_slope = new_slope
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||||
"window_auto_open_threshold": "Recommended value: 0.5. Leave empty if automatic window open detection is not use",
|
"window_auto_open_threshold": "Recommended value: between 0.05 and 0.1. Leave empty if automatic window open detection is not use",
|
||||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
|
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
|
||||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
|
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,15 @@ def skip_hass_states_get_fixture():
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="skip_control_heating")
|
||||||
|
def skip_control_heating_fixture():
|
||||||
|
"""Skip the control_heating of VersatileThermostat"""
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="skip_hass_states_is_state")
|
@pytest.fixture(name="skip_hass_states_is_state")
|
||||||
def skip_hass_states_is_state_fixture():
|
def skip_hass_states_is_state_fixture():
|
||||||
"""Skip the is_state in HomeAssistant"""
|
"""Skip the is_state in HomeAssistant"""
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from homeassistant.components.climate.const import (
|
""" The commons const for all tests """
|
||||||
|
from homeassistant.components.climate.const import ( # pylint: disable=unused-import
|
||||||
PRESET_BOOST,
|
PRESET_BOOST,
|
||||||
PRESET_COMFORT,
|
PRESET_COMFORT,
|
||||||
PRESET_ECO,
|
PRESET_ECO,
|
||||||
@@ -30,6 +31,9 @@ from custom_components.versatile_thermostat.const import (
|
|||||||
CONF_USE_PRESENCE_FEATURE,
|
CONF_USE_PRESENCE_FEATURE,
|
||||||
CONF_WINDOW_SENSOR,
|
CONF_WINDOW_SENSOR,
|
||||||
CONF_WINDOW_DELAY,
|
CONF_WINDOW_DELAY,
|
||||||
|
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
|
||||||
|
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
|
||||||
|
CONF_WINDOW_AUTO_MAX_DURATION,
|
||||||
CONF_MOTION_SENSOR,
|
CONF_MOTION_SENSOR,
|
||||||
CONF_MOTION_DELAY,
|
CONF_MOTION_DELAY,
|
||||||
CONF_MOTION_PRESET,
|
CONF_MOTION_PRESET,
|
||||||
@@ -95,6 +99,12 @@ MOCK_WINDOW_CONFIG = {
|
|||||||
CONF_WINDOW_DELAY: 10,
|
CONF_WINDOW_DELAY: 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MOCK_WINDOW_AUTO_CONFIG = {
|
||||||
|
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 1.0,
|
||||||
|
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.0,
|
||||||
|
CONF_WINDOW_AUTO_MAX_DURATION: 5.0,
|
||||||
|
}
|
||||||
|
|
||||||
MOCK_MOTION_CONFIG = {
|
MOCK_MOTION_CONFIG = {
|
||||||
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
|
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
|
||||||
CONF_MOTION_DELAY: 10,
|
CONF_MOTION_DELAY: 10,
|
||||||
|
|||||||
@@ -6,20 +6,7 @@ from homeassistant.config_entries import SOURCE_USER, ConfigEntry
|
|||||||
|
|
||||||
from custom_components.versatile_thermostat.const import DOMAIN
|
from custom_components.versatile_thermostat.const import DOMAIN
|
||||||
|
|
||||||
from .const import (
|
from .const import * # pylint: disable=wildcard-import, unused-wildcard-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:
|
async def test_show_form(hass: HomeAssistant) -> None:
|
||||||
@@ -217,3 +204,168 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
|
|||||||
assert result["result"].version == 1
|
assert result["result"].version == 1
|
||||||
assert result["result"].title == "TheOverClimateMockName"
|
assert result["result"].title == "TheOverClimateMockName"
|
||||||
assert isinstance(result["result"], ConfigEntry)
|
assert isinstance(result["result"], ConfigEntry)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_config_flow_window_auto_ok(
|
||||||
|
hass: HomeAssistant, skip_hass_states_get, skip_control_heating
|
||||||
|
):
|
||||||
|
"""Test the config flow with only window auto feature"""
|
||||||
|
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={
|
||||||
|
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_DEVICE_POWER: 1,
|
||||||
|
CONF_USE_WINDOW_FEATURE: True,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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_AUTO_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
|
||||||
|
| {
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_WINDOW_DELAY: 30, # the default value is added
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
}
|
||||||
|
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||||
|
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||||
|
| MOCK_PRESETS_CONFIG
|
||||||
|
| MOCK_WINDOW_AUTO_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_window_auto_ko(
|
||||||
|
hass: HomeAssistant, skip_hass_states_get
|
||||||
|
):
|
||||||
|
"""Test the config flow with window auto and window features -> not allowed"""
|
||||||
|
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={
|
||||||
|
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_DEVICE_POWER: 1,
|
||||||
|
CONF_USE_WINDOW_FEATURE: True,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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_AUTO_CONFIG | MOCK_WINDOW_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
# We should stay on window with an error
|
||||||
|
assert result["step_id"] == "window"
|
||||||
|
assert result["errors"] == {
|
||||||
|
"window_sensor_entity_id": "window_open_detection_method"
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ async def test_open_window_algo(
|
|||||||
the_algo = WindowOpenDetectionAlgorithm(1.0, 0.0)
|
the_algo = WindowOpenDetectionAlgorithm(1.0, 0.0)
|
||||||
assert the_algo.last_slope is None
|
assert the_algo.last_slope is None
|
||||||
|
|
||||||
tz = get_tz(hass)
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
now = datetime.now(tz)
|
now = datetime.now(tz)
|
||||||
|
|
||||||
event_timestamp = now - timedelta(minutes=5)
|
event_timestamp = now - timedelta(minutes=5)
|
||||||
@@ -50,7 +50,7 @@ async def test_open_window_algo(
|
|||||||
assert the_algo.is_window_close_detected() is False
|
assert the_algo.is_window_close_detected() is False
|
||||||
assert the_algo.is_window_open_detected() is False
|
assert the_algo.is_window_open_detected() is False
|
||||||
|
|
||||||
# A new temperature with 2 degre less
|
# A new temperature with 2 degre less in one minute (value will be rejected)
|
||||||
event_timestamp = now - timedelta(minutes=2)
|
event_timestamp = now - timedelta(minutes=2)
|
||||||
last_slope = the_algo.add_temp_measurement(
|
last_slope = the_algo.add_temp_measurement(
|
||||||
temperature=7, datetime_measure=event_timestamp
|
temperature=7, datetime_measure=event_timestamp
|
||||||
@@ -107,7 +107,7 @@ async def test_open_window_algo_wrong(
|
|||||||
the_algo = WindowOpenDetectionAlgorithm(1.0, 0.0)
|
the_algo = WindowOpenDetectionAlgorithm(1.0, 0.0)
|
||||||
assert the_algo.last_slope is None
|
assert the_algo.last_slope is None
|
||||||
|
|
||||||
tz = get_tz(hass)
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
now = datetime.now(tz)
|
now = datetime.now(tz)
|
||||||
|
|
||||||
event_timestamp = now - timedelta(minutes=5)
|
event_timestamp = now - timedelta(minutes=5)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
""" Test the Window management """
|
""" Test the Window management """
|
||||||
from unittest.mock import patch, call
|
import asyncio
|
||||||
|
from unittest.mock import patch, call, PropertyMock
|
||||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ logging.getLogger().setLevel(logging.DEBUG)
|
|||||||
async def test_window_management_time_not_enough(
|
async def test_window_management_time_not_enough(
|
||||||
hass: HomeAssistant, skip_hass_states_is_state
|
hass: HomeAssistant, skip_hass_states_is_state
|
||||||
):
|
):
|
||||||
"""Test the Power management"""
|
"""Test the Window management when time is not enough"""
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
@@ -97,7 +98,7 @@ async def test_window_management_time_not_enough(
|
|||||||
async def test_window_management_time_enough(
|
async def test_window_management_time_enough(
|
||||||
hass: HomeAssistant, skip_hass_states_is_state
|
hass: HomeAssistant, skip_hass_states_is_state
|
||||||
):
|
):
|
||||||
"""Test the Power management"""
|
"""Test the Window management when time is enough"""
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
@@ -197,3 +198,306 @@ async def test_window_management_time_enough(
|
|||||||
any_order=False,
|
any_order=False,
|
||||||
)
|
)
|
||||||
assert entity.preset_mode is PRESET_BOOST
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
|
||||||
|
|
||||||
|
async def test_window_auto_fast(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": 21,
|
||||||
|
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_AUTO_OPEN_THRESHOLD: 0.1,
|
||||||
|
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||||
|
CONF_WINDOW_AUTO_MAX_DURATION: 10, # Should be 0 for test
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
now = datetime.now(tz)
|
||||||
|
|
||||||
|
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 == 21
|
||||||
|
|
||||||
|
assert entity.window_state is None
|
||||||
|
|
||||||
|
# Make the temperature 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, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
event_timestamp = now - timedelta(minutes=4)
|
||||||
|
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||||
|
|
||||||
|
# The heater turns on
|
||||||
|
assert mock_send_event.call_count == 0
|
||||||
|
assert mock_heater_on.call_count == 1
|
||||||
|
assert entity.last_temperature_slope is None
|
||||||
|
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||||
|
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||||
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
|
|
||||||
|
# send one degre down in one minute
|
||||||
|
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(
|
||||||
|
"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)
|
||||||
|
|
||||||
|
# The heater turns on
|
||||||
|
assert mock_send_event.call_count == 2
|
||||||
|
assert mock_heater_on.call_count == 0
|
||||||
|
assert mock_heater_off.call_count >= 1
|
||||||
|
assert entity.last_temperature_slope == -1
|
||||||
|
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||||
|
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||||
|
assert entity.window_auto_state == STATE_ON
|
||||||
|
assert entity.hvac_mode is HVACMode.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,
|
||||||
|
)
|
||||||
|
|
||||||
|
# send another 0.1 degre in one minute -> no change
|
||||||
|
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(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||||
|
new_callable=PropertyMock,
|
||||||
|
return_value=False,
|
||||||
|
):
|
||||||
|
event_timestamp = now - timedelta(minutes=2)
|
||||||
|
await send_temperature_change_event(entity, 17.9, event_timestamp)
|
||||||
|
|
||||||
|
# The heater turns on
|
||||||
|
assert mock_send_event.call_count == 0
|
||||||
|
assert mock_heater_on.call_count == 0
|
||||||
|
assert mock_heater_off.call_count == 0
|
||||||
|
assert round(entity.last_temperature_slope, 3) == -0.1 * 0.5 - 1 * 0.5
|
||||||
|
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||||
|
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||||
|
assert entity.window_auto_state == STATE_ON
|
||||||
|
assert entity.hvac_mode is HVACMode.OFF
|
||||||
|
|
||||||
|
# send another plus 1.1 degre in one minute -> restore 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"
|
||||||
|
) as mock_heater_on, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||||
|
) as mock_heater_off, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||||
|
new_callable=PropertyMock,
|
||||||
|
return_value=False,
|
||||||
|
):
|
||||||
|
event_timestamp = now - timedelta(minutes=1)
|
||||||
|
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||||
|
|
||||||
|
# The heater turns on
|
||||||
|
assert mock_send_event.call_count == 2
|
||||||
|
mock_send_event.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.send_event(
|
||||||
|
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT}
|
||||||
|
),
|
||||||
|
call.send_event(
|
||||||
|
EventType.WINDOW_AUTO_EVENT,
|
||||||
|
{
|
||||||
|
"type": "end",
|
||||||
|
"cause": "end of slope alert",
|
||||||
|
"curve_slope": 0.27500000000000036,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
any_order=True,
|
||||||
|
)
|
||||||
|
assert mock_heater_on.call_count == 1
|
||||||
|
assert mock_heater_off.call_count == 0
|
||||||
|
assert round(entity.last_temperature_slope, 3) == 0.275
|
||||||
|
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||||
|
assert entity._window_auto_algo.is_window_close_detected() is True
|
||||||
|
assert entity.window_auto_state == STATE_OFF
|
||||||
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_window_auto_auto_stop(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": 21,
|
||||||
|
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_AUTO_OPEN_THRESHOLD: 0.1,
|
||||||
|
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
|
||||||
|
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
now = datetime.now(tz)
|
||||||
|
|
||||||
|
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 == 21
|
||||||
|
|
||||||
|
assert entity.window_state is None
|
||||||
|
|
||||||
|
# Make the temperature 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, patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
event_timestamp = now - timedelta(minutes=4)
|
||||||
|
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||||
|
|
||||||
|
# The heater turns on
|
||||||
|
assert mock_heater_on.call_count == 1
|
||||||
|
assert entity.last_temperature_slope is None
|
||||||
|
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||||
|
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||||
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
|
|
||||||
|
# send one degre down in one minute
|
||||||
|
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(
|
||||||
|
"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)
|
||||||
|
|
||||||
|
# The heater turns on
|
||||||
|
assert mock_send_event.call_count == 2
|
||||||
|
assert mock_heater_on.call_count == 0
|
||||||
|
assert mock_heater_off.call_count >= 1
|
||||||
|
assert entity.last_temperature_slope == -1
|
||||||
|
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||||
|
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||||
|
assert entity.window_auto_state == STATE_ON
|
||||||
|
assert entity.hvac_mode is HVACMode.OFF
|
||||||
|
|
||||||
|
# Waits for automatic disable
|
||||||
|
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(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
|
||||||
|
return_value=False,
|
||||||
|
):
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
assert mock_heater_on.call_count == 1
|
||||||
|
assert mock_heater_off.call_count == 0
|
||||||
|
assert round(entity.last_temperature_slope, 3) == -1
|
||||||
|
# Because the algorithm is not aware of the expiration, for the algo we are still in alert
|
||||||
|
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||||
|
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||||
|
assert entity.window_auto_state == STATE_OFF
|
||||||
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
"window_sensor_entity_id": "Leave empty if no window sensor should be use",
|
||||||
"window_auto_open_threshold": "Recommended value: 0.5. Leave empty if automatic window open detection is not use",
|
"window_auto_open_threshold": "Recommended value: between 0.05 and 0.1. Leave empty if automatic window open detection is not use",
|
||||||
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
|
"window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use",
|
||||||
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
|
"window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
|
"window_sensor_entity_id": "Laissez vide si vous n'avez de détecteur",
|
||||||
"window_auto_open_threshold": "Valeur recommandée: 0.5. Laissez vide si vous n'utilisez pas la détection automatique",
|
"window_auto_open_threshold": "Valeur recommandée: entre 0.05 et 0.1. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||||
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
|
"window_auto_close_threshold": "Valeur recommandée: 0. Laissez vide si vous n'utilisez pas la détection automatique",
|
||||||
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique"
|
"window_auto_max_duration": "Valeur recommandée: 60 (1 heure). Laissez vide si vous n'utilisez pas la détection automatique"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user