diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c3c0b24..cd93742 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -30,7 +30,8 @@ "waderyan.gitblame", "keesschollaart.vscode-home-assistant", "vscode.markdown-math", - "yzhang.markdown-all-in-one" + "yzhang.markdown-all-in-one", + "ms-python.vscode-pylance" ], // "mounts": [ // "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=${localWorkspaceFolder}/config/www/community/,type=bind,consistency=cached", diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 62f9b6c..23f39cc 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -281,7 +281,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): def clean_one(cfg, schema: vol.Schema): """Clean one schema""" for key, _ in schema.schema.items(): - if key in cfg is not None: + if key in cfg: del cfg[key] cfg = config_entry.copy() @@ -301,7 +301,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): clean_one(cfg, STEP_CENTRAL_WINDOW_DATA_SCHEMA) if cfg.get(CONF_USE_MOTION_CENTRAL_CONFIG) is True: - clean_one(cfg, STEP_CENTRAL_WINDOW_DATA_SCHEMA) + clean_one(cfg, STEP_CENTRAL_MOTION_DATA_SCHEMA) if cfg.get(CONF_USE_POWER_CENTRAL_CONFIG) is True: clean_one(cfg, STEP_CENTRAL_POWER_DATA_SCHEMA) diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py index 7e2f9e6..5ee3500 100644 --- a/custom_components/versatile_thermostat/climate.py +++ b/custom_components/versatile_thermostat/climate.py @@ -80,6 +80,7 @@ async def async_setup_entry( async_add_entities([entity], True) # Add services + # TODO move this to async_setup ? platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_PRESENCE, diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index 0c21b57..0ee5802 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -127,7 +127,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): else: self._infos[config] = self._central_config is not None - self._infos[COMES_FROM] = None + if COMES_FROM in self._infos: + del self._infos[COMES_FROM] async def validate_input(self, data: dict) -> None: """Validate the user input allows us to connect. @@ -156,7 +157,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): raise UnknownEntity(conf) # Check that only one window feature is used - ws = data.get(CONF_WINDOW_SENSOR) # pylint: disable=invalid-name + ws = self._infos.get(CONF_WINDOW_SENSOR) # pylint: disable=invalid-name waot = data.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) wact = data.get(CONF_WINDOW_AUTO_CLOSE_THRESHOLD) wamd = data.get(CONF_WINDOW_AUTO_MAX_DURATION) @@ -315,6 +316,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: schema = STEP_CENTRAL_TPI_DATA_SCHEMA next_step = self.async_step_presets + elif self._infos.get(COMES_FROM) == "async_step_spec_tpi": + schema = STEP_CENTRAL_TPI_DATA_SCHEMA return await self.generic_step("tpi", schema, user_input, next_step) @@ -323,6 +326,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): _LOGGER.debug("Into ConfigFlow.async_step_spec_tpi user_input=%s", user_input) schema = STEP_CENTRAL_TPI_DATA_SCHEMA + self._infos[COMES_FROM] = "async_step_spec_tpi" next_step = self.async_step_presets return await self.generic_step("tpi", schema, user_input, next_step) @@ -563,7 +567,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): schema = STEP_CENTRAL_ADVANCED_DATA_SCHEMA - self._infos[COMES_FROM] = "async_step_spec_presence" + self._infos[COMES_FROM] = "async_step_spec_advanced" next_step = self.async_step_advanced @@ -607,7 +611,8 @@ class VersatileThermostatConfigFlow( """Finalization of the ConfigEntry creation""" _LOGGER.debug("ConfigFlow.async_finalize") # Removes temporary value - self._infos[COMES_FROM] = None + if COMES_FROM in self._infos: + del self._infos[COMES_FROM] return self.async_create_entry(title=self._infos[CONF_NAME], data=self._infos) @@ -811,7 +816,8 @@ class VersatileThermostatOptionsFlowHandler( ) # Removes temporary value - self._infos[COMES_FROM] = None + if COMES_FROM in self._infos: + del self._infos[COMES_FROM] self.hass.config_entries.async_update_entry(self.config_entry, data=self._infos) return self.async_create_entry(title=None, data=None) diff --git a/custom_components/versatile_thermostat/prop_algorithm.py b/custom_components/versatile_thermostat/prop_algorithm.py index f4f78df..e595dbc 100644 --- a/custom_components/versatile_thermostat/prop_algorithm.py +++ b/custom_components/versatile_thermostat/prop_algorithm.py @@ -25,7 +25,7 @@ class PropAlgorithm: ) -> None: """Initialisation of the Proportional Algorithm""" _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", # pylint: disable=line-too-long function_type, tpi_coef_int, tpi_coef_ext, diff --git a/tests/commons.py b/tests/commons.py index c2a8514..9a1783a 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -29,6 +29,8 @@ from custom_components.versatile_thermostat.commons import ( # pylint: disable= NowClass, ) +from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI + from .const import ( # pylint: disable=unused-import MOCK_TH_OVER_SWITCH_USER_CONFIG, MOCK_TH_OVER_4SWITCH_USER_CONFIG, @@ -56,6 +58,7 @@ from .const import ( # pylint: disable=unused-import PRESET_ACTIVITY, ) + FULL_SWITCH_CONFIG = ( MOCK_TH_OVER_SWITCH_USER_CONFIG | MOCK_TH_OVER_SWITCH_TYPE_CONFIG @@ -114,6 +117,45 @@ FULL_4SWITCH_CONFIG = ( | MOCK_ADVANCED_CONFIG ) +FULL_CENTRAL_CONFIG = { + CONF_NAME: CENTRAL_CONFIG_NAME, + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG, + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", + CONF_TEMP_MIN: 15, + CONF_TEMP_MAX: 30, + CONF_TPI_COEF_INT: 0.5, + CONF_TPI_COEF_EXT: 0.02, + "frost_temp": 10, + "eco_temp": 17.1, + "comfort_temp": 0, + "boost_temp": 19.1, + "eco_ac_temp": 25.1, + "comfort_ac_temp": 23.1, + "boost_ac_temp": 21.1, + "frost_away_temp": 15.1, + "eco_away_temp": 15.2, + "comfort_away_temp": 0, + "boost_away_temp": 15.4, + "eco_ac_away_temp": 30.5, + "comfort_ac_away_temp": 0, + "boost_ac_away_temp": 30.7, + CONF_WINDOW_DELAY: 15, + CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4, + CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1, + CONF_WINDOW_AUTO_MAX_DURATION: 31, + CONF_MOTION_DELAY: 31, + CONF_MOTION_OFF_DELAY: 301, + CONF_MOTION_PRESET: "boost", + CONF_NO_MOTION_PRESET: "frost", + CONF_POWER_SENSOR: "sensor.mock_power_sensor", + CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor", + CONF_PRESET_POWER: 14, + CONF_MINIMAL_ACTIVATION_DELAY: 11, + CONF_SECURITY_DELAY_MIN: 61, + CONF_SECURITY_MIN_ON_PERCENT: 0.5, + CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2, +} + _LOGGER = logging.getLogger(__name__) @@ -302,6 +344,27 @@ async def create_thermostat( return search_entity(hass, entity_id, CLIMATE_DOMAIN) +async def create_central_config( # pylint: disable=dangerous-default-value + hass: HomeAssistant, entry: MockConfigEntry = FULL_CENTRAL_CONFIG +): + """Creates a Central Configuration from entry given in argument""" + central_config_entry = MockConfigEntry( + domain=DOMAIN, + title="TheCentralConfigMockName", + unique_id="centralConfigUniqueId", + data=entry, + ) + + central_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(central_config_entry.entry_id) + assert central_config_entry.state is ConfigEntryState.LOADED + + # Test that VTherm API find the CentralConfig + api = VersatileThermostatAPI.get_vtherm_api(hass) + central_configuration = api.find_central_configuration() + assert central_configuration is not None + + def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity: """Search and return the entity in the domain""" component = hass.data[domain] @@ -311,6 +374,12 @@ def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity: return None +def count_entities(hass: HomeAssistant, entity_id, domain) -> Entity: + """Search and return the entity in the domain""" + component = hass.data[domain] + return len(list(component.entities)) if component.entities else 0 + + async def send_temperature_change_event( entity: BaseThermostat, new_temp, date, sleep=True ): diff --git a/tests/const.py b/tests/const.py index 32a6d27..9d4cd09 100644 --- a/tests/const.py +++ b/tests/const.py @@ -6,74 +6,23 @@ from homeassistant.components.climate.const import ( # pylint: disable=unused-i PRESET_NONE, PRESET_ACTIVITY, ) -from custom_components.versatile_thermostat.const import ( - CONF_NAME, - CONF_HEATER, - CONF_HEATER_2, - CONF_HEATER_3, - CONF_HEATER_4, - CONF_THERMOSTAT_CLIMATE, - CONF_THERMOSTAT_SWITCH, - CONF_THERMOSTAT_TYPE, - CONF_AC_MODE, - 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_WINDOW_AUTO_OPEN_THRESHOLD, - CONF_WINDOW_AUTO_CLOSE_THRESHOLD, - CONF_WINDOW_AUTO_MAX_DURATION, - CONF_MOTION_SENSOR, - CONF_MOTION_DELAY, - CONF_MOTION_OFF_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, - CONF_AUTO_REGULATION_MODE, - CONF_AUTO_REGULATION_STRONG, - CONF_AUTO_REGULATION_NONE, - CONF_AUTO_REGULATION_DTEMP, - CONF_AUTO_REGULATION_PERIOD_MIN, - CONF_INVERSE_SWITCH, - CONF_AUTO_FAN_HIGH, - CONF_AUTO_FAN_MODE, - PRESET_FROST_PROTECTION, -) + +from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import MOCK_TH_OVER_SWITCH_USER_CONFIG = { - CONF_NAME: "TheOverSwitchMockName", CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, +} + +MOCK_TH_OVER_SWITCH_MAIN_CONFIG = { + CONF_NAME: "TheOverSwitchMockName", 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: True, CONF_USE_POWER_FEATURE: True, CONF_USE_PRESENCE_FEATURE: True, + CONF_USE_MAIN_CENTRAL_CONFIG: True, } MOCK_TH_OVER_4SWITCH_USER_CONFIG = { @@ -92,14 +41,23 @@ MOCK_TH_OVER_4SWITCH_USER_CONFIG = { } MOCK_TH_OVER_CLIMATE_USER_CONFIG = { - CONF_NAME: "TheOverClimateMockName", CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE, +} + + +MOCK_TH_OVER_CLIMATE_MAIN_CONFIG = { + CONF_NAME: "TheOverClimateMockName", CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", - CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", CONF_CYCLE_MIN: 5, + CONF_DEVICE_POWER: 1, + CONF_USE_MAIN_CENTRAL_CONFIG: False + # Keep default values which are False +} + +MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG = { + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", CONF_TEMP_MIN: 15, CONF_TEMP_MAX: 30, - CONF_DEVICE_POWER: 1 # Keep default values which are False } @@ -228,3 +186,14 @@ MOCK_DEFAULT_FEATURE_CONFIG = { CONF_USE_POWER_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False, } + +MOCK_DEFAULT_CENTRAL_CONFIG = { + CONF_USE_MAIN_CENTRAL_CONFIG: False, + CONF_USE_TPI_CENTRAL_CONFIG: False, + CONF_USE_PRESETS_CENTRAL_CONFIG: False, + CONF_USE_WINDOW_CENTRAL_CONFIG: False, + CONF_USE_MOTION_CENTRAL_CONFIG: False, + CONF_USE_POWER_CENTRAL_CONFIG: False, + CONF_USE_PRESENCE_CENTRAL_CONFIG: False, + CONF_USE_ADVANCED_CENTRAL_CONFIG: False, +} diff --git a/tests/test_central_config.py b/tests/test_central_config.py new file mode 100644 index 0000000..4380a45 --- /dev/null +++ b/tests/test_central_config.py @@ -0,0 +1,400 @@ +# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long + +""" Test the central_configuration """ +from unittest.mock import patch # , call + +# from datetime import datetime # , timedelta + +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 custom_components.versatile_thermostat.base_thermostat import BaseThermostat +from custom_components.versatile_thermostat.thermostat_climate import ( + ThermostatOverClimate, +) + +from custom_components.versatile_thermostat.thermostat_switch import ( + ThermostatOverSwitch, +) + +from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI + +from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import + + +# @pytest.mark.parametrize("expected_lingering_tasks", [True]) +# @pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_state): + """Tests the clean_central_config_doubon of base_thermostat""" + central_config_entry = MockConfigEntry( + domain=DOMAIN, + title="TheCentralConfigMockName", + unique_id="centralConfigUniqueId", + data={ + CONF_NAME: CENTRAL_CONFIG_NAME, + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG, + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_central_ext_temp_sensor", + CONF_TEMP_MIN: 15, + CONF_TEMP_MAX: 30, + CONF_TPI_COEF_INT: 0.5, + CONF_TPI_COEF_EXT: 0.02, + "frost_temp": 10, + "eco_temp": 17.1, + "comfort_temp": 18.1, + "boost_temp": 19.1, + "eco_ac_temp": 25.1, + "comfort_ac_temp": 23.1, + "boost_ac_temp": 21.1, + "frost_away_temp": 15.1, + "eco_away_temp": 15.2, + "comfort_away_temp": 15.3, + "boost_away_temp": 15.4, + "eco_ac_away_temp": 30.5, + "comfort_ac_away_temp": 30.6, + "boost_ac_away_temp": 30.7, + CONF_WINDOW_DELAY: 15, + CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4, + CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1, + CONF_WINDOW_AUTO_MAX_DURATION: 31, + CONF_MOTION_DELAY: 31, + CONF_MOTION_OFF_DELAY: 301, + CONF_MOTION_PRESET: "boost", + CONF_NO_MOTION_PRESET: "frost", + CONF_POWER_SENSOR: "sensor.mock_central_power_sensor", + CONF_MAX_POWER_SENSOR: "sensor.mock_central_max_power_sensor", + CONF_PRESET_POWER: 14, + CONF_MINIMAL_ACTIVATION_DELAY: 11, + CONF_SECURITY_DELAY_MIN: 61, + CONF_SECURITY_MIN_ON_PERCENT: 0.5, + CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2, + }, + ) + + central_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(central_config_entry.entry_id) + assert central_config_entry.state is ConfigEntryState.LOADED + + entity: ThermostatOverClimate = search_entity( + hass, "climate.thecentralconfigmockname", "climate" + ) + + assert entity is None + + assert count_entities(hass, "climate.thecentralconfigmockname", "climate") == 0 + + # Test that VTherm API find the CentralConfig + api = VersatileThermostatAPI.get_vtherm_api(hass) + central_configuration = api.find_central_configuration() + assert central_configuration is not None + + +# @pytest.mark.parametrize("expected_lingering_tasks", [True]) +# @pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_minimal_over_switch_wo_central_config( + hass: HomeAssistant, skip_hass_states_is_state +): + """Tests that a VTherm without any central_configuration is working with its own attributes""" + create_central_config(hass) + + # Add a Switch VTherm + 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: 8, + CONF_TEMP_MAX: 18, + "frost_temp": 10, + "eco_temp": 17, + "comfort_temp": 18, + "boost_temp": 21, + 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_SECURITY_DEFAULT_ON_PERCENT: 0.1, + # 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 + CONF_INVERSE_SWITCH: True, + # CONF_USE_MAIN_CENTRAL_CONFIG: False, + # CONF_USE_TPI_CENTRAL_CONFIG: False, + # CONF_USE_WINDOW_CENTRAL_CONFIG: False, + # CONF_USE_MOTION_CENTRAL_CONFIG: False, + # CONF_USE_POWER_CENTRAL_CONFIG: False, + # CONF_USE_PRESENCE_CENTRAL_CONFIG: False, + # CONF_USE_PRESETS_CENTRAL_CONFIG: False, + # CONF_USE_ADVANCED_CENTRAL_CONFIG: False, + }, + ) + + with patch("homeassistant.core.ServiceRegistry.async_call"): + entity: ThermostatOverSwitch = await create_thermostat( + hass, entry, "climate.theoverswitchmockname" + ) + assert entity + assert entity.name == "TheOverSwitchMockName" + assert entity.is_over_switch + assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor" + assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor" + assert entity._cycle_min == 5 + assert entity._attr_min_temp == 8 + assert entity._attr_max_temp == 18 + assert entity.preset_modes == ["none", "frost", "eco", "comfort", "boost"] + assert entity.is_window_auto_enabled is False + assert entity.nb_underlying_entities == 1 + assert entity.underlying_entity_id(0) == "switch.mock_switch" + assert entity.proportional_algorithm is not None + assert entity.proportional_algorithm._tpi_coef_int == 0.3 + assert entity.proportional_algorithm._tpi_coef_ext == 0.01 + assert entity.proportional_algorithm._minimal_activation_delay == 30 + assert entity._security_delay_min == 5 + assert entity._security_min_on_percent == 0.3 + assert entity._security_default_on_percent == 0.1 + assert entity.is_inversed + + +# @pytest.mark.parametrize("expected_lingering_tasks", [True]) +# @pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_full_over_switch_wo_central_config( + hass: HomeAssistant, skip_hass_states_is_state +): + """Tests that a VTherm without any central_configuration is working with its own attributes""" + await create_central_config(hass) + + # Add a Switch VTherm + 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: 8, + CONF_TEMP_MAX: 18, + "frost_temp": 10, + "eco_temp": 17, + "comfort_temp": 18, + "boost_temp": 21, + "frost_away_temp": 13, + "eco_away_temp": 13, + "comfort_away_temp": 13, + "boost_away_temp": 13, + CONF_USE_WINDOW_FEATURE: True, + CONF_USE_MOTION_FEATURE: True, + CONF_USE_POWER_FEATURE: True, + CONF_USE_PRESENCE_FEATURE: True, + CONF_HEATER: "switch.mock_switch", + CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, + CONF_INVERSE_SWITCH: False, + 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_SECURITY_DEFAULT_ON_PERCENT: 0.1, + CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor", + CONF_WINDOW_DELAY: 30, + CONF_WINDOW_AUTO_OPEN_THRESHOLD: 3, + CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1, + CONF_WINDOW_AUTO_MAX_DURATION: 5, + CONF_MOTION_DELAY: 10, + CONF_MOTION_OFF_DELAY: 29, + CONF_MOTION_PRESET: "comfort", + CONF_NO_MOTION_PRESET: "eco", + CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor", + CONF_POWER_SENSOR: "sensor.mock_power_sensor", + CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor", + CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor", + CONF_USE_MAIN_CENTRAL_CONFIG: False, + CONF_USE_TPI_CENTRAL_CONFIG: False, + CONF_USE_WINDOW_CENTRAL_CONFIG: False, + CONF_USE_MOTION_CENTRAL_CONFIG: False, + CONF_USE_POWER_CENTRAL_CONFIG: False, + CONF_USE_PRESENCE_CENTRAL_CONFIG: False, + CONF_USE_PRESETS_CENTRAL_CONFIG: False, + CONF_USE_ADVANCED_CENTRAL_CONFIG: False, + }, + ) + + with patch("homeassistant.core.ServiceRegistry.async_call"): + entity: ThermostatOverSwitch = await create_thermostat( + hass, entry, "climate.theoverswitchmockname" + ) + assert entity + assert entity.name == "TheOverSwitchMockName" + assert entity.is_over_switch + assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor" + assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor" + assert entity._cycle_min == 5 + assert entity._attr_min_temp == 8 + assert entity._attr_max_temp == 18 + assert entity.preset_modes == [ + "none", + "frost", + "eco", + "comfort", + "boost", + "activity", + ] + assert entity.nb_underlying_entities == 1 + assert entity.underlying_entity_id(0) == "switch.mock_switch" + assert entity.proportional_algorithm is not None + assert entity.proportional_algorithm._tpi_coef_int == 0.3 + assert entity.proportional_algorithm._tpi_coef_ext == 0.01 + assert entity.proportional_algorithm._minimal_activation_delay == 30 + assert entity._security_delay_min == 5 + assert entity._security_min_on_percent == 0.3 + assert entity._security_default_on_percent == 0.1 + assert entity.is_inversed is False + + assert entity.is_window_auto_enabled is True + assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor" + assert entity._window_delay_sec == 30 + assert entity._window_auto_close_threshold == 0.1 + assert entity._window_auto_open_threshold == 3 + assert entity._window_auto_max_duration == 5 + + assert entity._motion_sensor_entity_id == "binary_sensor.mock_motion_sensor" + assert entity._motion_delay_sec == 10 + assert entity._motion_off_delay_sec == 29 + assert entity._motion_preset == "comfort" + assert entity._no_motion_preset == "eco" + + assert entity._power_sensor_entity_id == "sensor.mock_power_sensor" + assert entity._max_power_sensor_entity_id == "sensor.mock_max_power_sensor" + + assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor" + + +# @pytest.mark.parametrize("expected_lingering_tasks", [True]) +# @pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_full_over_switch_with_central_config( + hass: HomeAssistant, skip_hass_states_is_state +): + """Tests that a VTherm with central_configuration is working with the central_config attributes""" + await create_central_config(hass) + + # Add a Switch VTherm + 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: 8, + CONF_TEMP_MAX: 18, + "frost_temp": 10, + "eco_temp": 17, + "comfort_temp": 18, + "boost_temp": 21, + CONF_USE_WINDOW_FEATURE: True, + CONF_USE_MOTION_FEATURE: True, + CONF_USE_POWER_FEATURE: True, + CONF_USE_PRESENCE_FEATURE: True, + CONF_HEATER: "switch.mock_switch", + CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, + CONF_INVERSE_SWITCH: False, + 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_SECURITY_DEFAULT_ON_PERCENT: 0.1, + CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor", + CONF_WINDOW_DELAY: 30, + CONF_WINDOW_AUTO_OPEN_THRESHOLD: 3, + CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1, + CONF_WINDOW_AUTO_MAX_DURATION: 5, + CONF_MOTION_DELAY: 10, + CONF_MOTION_OFF_DELAY: 29, + CONF_MOTION_PRESET: "comfort", + CONF_NO_MOTION_PRESET: "eco", + CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor", + CONF_POWER_SENSOR: "sensor.mock_power_sensor", + CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor", + CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor", + CONF_USE_MAIN_CENTRAL_CONFIG: True, + CONF_USE_TPI_CENTRAL_CONFIG: True, + CONF_USE_WINDOW_CENTRAL_CONFIG: True, + CONF_USE_MOTION_CENTRAL_CONFIG: True, + CONF_USE_POWER_CENTRAL_CONFIG: True, + CONF_USE_PRESENCE_CENTRAL_CONFIG: True, + CONF_USE_PRESETS_CENTRAL_CONFIG: True, + CONF_USE_ADVANCED_CENTRAL_CONFIG: True, + }, + ) + + with patch("homeassistant.core.ServiceRegistry.async_call"): + entity: ThermostatOverSwitch = await create_thermostat( + hass, entry, "climate.theoverswitchmockname" + ) + assert entity + assert entity.name == "TheOverSwitchMockName" + assert entity.is_over_switch + assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor" + assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor" + assert entity._cycle_min == 5 + assert entity._attr_min_temp == 15 + assert entity._attr_max_temp == 30 + assert entity.preset_modes == [ + "none", + "frost", + "eco", + "boost", + "activity", + ] + assert entity.nb_underlying_entities == 1 + assert entity.underlying_entity_id(0) == "switch.mock_switch" + assert entity.proportional_algorithm is not None + assert entity.proportional_algorithm._tpi_coef_int == 0.5 + assert entity.proportional_algorithm._tpi_coef_ext == 0.02 + assert entity.proportional_algorithm._minimal_activation_delay == 11 + assert entity._security_delay_min == 61 + assert entity._security_min_on_percent == 0.5 + assert entity._security_default_on_percent == 0.2 + assert entity.is_inversed is False + + assert entity.is_window_auto_enabled is True + assert entity._window_sensor_entity_id == "binary_sensor.mock_window_sensor" + assert entity._window_delay_sec == 15 + assert entity._window_auto_close_threshold == 1 + assert entity._window_auto_open_threshold == 4 + assert entity._window_auto_max_duration == 31 + + assert entity._motion_sensor_entity_id == "binary_sensor.mock_motion_sensor" + assert entity._motion_delay_sec == 31 + assert entity._motion_off_delay_sec == 301 + assert entity._motion_preset == "boost" + assert entity._no_motion_preset == "frost" + + assert entity._power_sensor_entity_id == "sensor.mock_power_sensor" + assert entity._max_power_sensor_entity_id == "sensor.mock_max_power_sensor" + + assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor" diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 69d82e6..b93216f 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -19,6 +19,7 @@ async def test_show_form(hass: HomeAssistant) -> None: # hass.data["custom_components"] = None # loader.async_get_custom_components(hass) # BaseThermostatAPI(hass) + await create_central_config(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -35,7 +36,8 @@ async def test_show_form(hass: HomeAssistant) -> None: async def test_user_config_flow_over_switch( hass: HomeAssistant, skip_hass_states_get ): # pylint: disable=unused-argument - """Test the config flow with all thermostat_over_switch features""" + """Test the config flow with all thermostat_over_switch features and central config on""" + await create_central_config(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -47,6 +49,14 @@ async def test_user_config_flow_over_switch( result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_USER_CONFIG ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "main" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_MAIN_CONFIG + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "type" assert result["errors"] == {} @@ -60,7 +70,7 @@ async def test_user_config_flow_over_switch( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG + result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -68,7 +78,7 @@ async def test_user_config_flow_over_switch( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_PRESETS_CONFIG + result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -76,7 +86,11 @@ async def test_user_config_flow_over_switch( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_WINDOW_CONFIG + result["flow_id"], + user_input={ + CONF_WINDOW_SENSOR: "binary_sensor.window_sensor", + CONF_USE_WINDOW_CENTRAL_CONFIG: True, + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -84,7 +98,11 @@ async def test_user_config_flow_over_switch( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_MOTION_CONFIG + result["flow_id"], + user_input={ + CONF_MOTION_SENSOR: "input_boolean.motion_sensor", + CONF_USE_MOTION_CENTRAL_CONFIG: True, + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -92,7 +110,7 @@ async def test_user_config_flow_over_switch( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_POWER_CONFIG + result["flow_id"], user_input={CONF_USE_POWER_CENTRAL_CONFIG: True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -100,7 +118,11 @@ async def test_user_config_flow_over_switch( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_PRESENCE_CONFIG + result["flow_id"], + user_input={ + CONF_PRESENCE_SENSOR: "person.presence_sensor", + CONF_USE_PRESENCE_CENTRAL_CONFIG: True, + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -108,21 +130,27 @@ async def test_user_config_flow_over_switch( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_ADVANCED_CONFIG + result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert ( - result["data"] - == MOCK_TH_OVER_SWITCH_USER_CONFIG + assert result["data"] == ( + MOCK_TH_OVER_SWITCH_USER_CONFIG + | MOCK_TH_OVER_SWITCH_MAIN_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 + | {CONF_WINDOW_SENSOR: "binary_sensor.window_sensor"} + | {CONF_MOTION_SENSOR: "input_boolean.motion_sensor"} + | {CONF_PRESENCE_SENSOR: "person.presence_sensor"} + | { + CONF_USE_MAIN_CENTRAL_CONFIG: True, + CONF_USE_TPI_CENTRAL_CONFIG: True, + CONF_USE_PRESETS_CENTRAL_CONFIG: True, + CONF_USE_WINDOW_CENTRAL_CONFIG: True, + CONF_USE_MOTION_CENTRAL_CONFIG: True, + CONF_USE_POWER_CENTRAL_CONFIG: True, + CONF_USE_PRESENCE_CENTRAL_CONFIG: True, + CONF_USE_ADVANCED_CENTRAL_CONFIG: True, + } ) assert result["result"] assert result["result"].domain == DOMAIN @@ -137,6 +165,8 @@ async def test_user_config_flow_over_climate( hass: HomeAssistant, skip_hass_states_get ): # pylint: disable=unused-argument """Test the config flow with all thermostat_over_climate features and no additional features""" + await create_central_config(hass) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -148,6 +178,22 @@ async def test_user_config_flow_over_climate( result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_USER_CONFIG ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "main" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_MAIN_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "main" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "type" assert result["errors"] == {} @@ -160,6 +206,14 @@ async def test_user_config_flow_over_climate( assert result["step_id"] == "presets" assert result["errors"] == {} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: False} + ) + + 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 ) @@ -168,19 +222,31 @@ async def test_user_config_flow_over_climate( assert result["step_id"] == "advanced" assert result["errors"] == {} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: False} + ) + + 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[ + "data" + ] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | MOCK_PRESETS_CONFIG | MOCK_ADVANCED_CONFIG | MOCK_DEFAULT_FEATURE_CONFIG | { + CONF_USE_MAIN_CENTRAL_CONFIG: False, + CONF_USE_TPI_CENTRAL_CONFIG: False, + CONF_USE_PRESETS_CENTRAL_CONFIG: False, + CONF_USE_WINDOW_CENTRAL_CONFIG: False, + CONF_USE_MOTION_CENTRAL_CONFIG: False, + CONF_USE_POWER_CENTRAL_CONFIG: False, + CONF_USE_PRESENCE_CENTRAL_CONFIG: False, + CONF_USE_ADVANCED_CENTRAL_CONFIG: False, + } assert result["result"] assert result["result"].domain == DOMAIN assert result["result"].version == 1 @@ -196,6 +262,8 @@ async def test_user_config_flow_window_auto_ok( skip_control_heating, # pylint: disable=unused-argument ): """Test the config flow with only window auto feature""" + await create_central_config(hass) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -206,18 +274,26 @@ async def test_user_config_flow_window_auto_ok( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - CONF_NAME: "TheOverSwitchMockName", CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "main" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "TheOverSwitchMockName", 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, + CONF_USE_MAIN_CENTRAL_CONFIG: True, }, ) @@ -233,6 +309,14 @@ async def test_user_config_flow_window_auto_ok( assert result["step_id"] == "tpi" assert result["errors"] == {} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False} + ) + + 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 ) @@ -242,7 +326,16 @@ async def test_user_config_flow_window_auto_ok( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_PRESETS_CONFIG + result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} + ) + + 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={CONF_USE_WINDOW_CENTRAL_CONFIG: False}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -259,27 +352,34 @@ async def test_user_config_flow_window_auto_ok( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_ADVANCED_CONFIG + result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True} ) 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["data"] == { + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, + CONF_NAME: "TheOverSwitchMockName", + CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", + CONF_CYCLE_MIN: 5, + CONF_DEVICE_POWER: 1, + CONF_USE_WINDOW_FEATURE: True, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_WINDOW_DELAY: 30, # the default value is added + } | MOCK_TH_OVER_SWITCH_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_WINDOW_AUTO_CONFIG | { + CONF_USE_MAIN_CENTRAL_CONFIG: True, + CONF_USE_TPI_CENTRAL_CONFIG: False, + CONF_USE_PRESETS_CENTRAL_CONFIG: True, + CONF_USE_WINDOW_CENTRAL_CONFIG: False, + CONF_USE_MOTION_CENTRAL_CONFIG: False, + CONF_USE_POWER_CENTRAL_CONFIG: False, + CONF_USE_PRESENCE_CENTRAL_CONFIG: False, + CONF_USE_ADVANCED_CENTRAL_CONFIG: True, + } assert result["result"] assert result["result"].domain == DOMAIN assert result["result"].version == 1 @@ -293,6 +393,9 @@ async def test_user_config_flow_window_auto_ko( hass: HomeAssistant, skip_hass_states_get # pylint: disable=unused-argument ): """Test the config flow with window auto and window features -> not allowed""" + + await create_central_config(hass) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -303,18 +406,26 @@ async def test_user_config_flow_window_auto_ko( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - CONF_NAME: "TheOverSwitchMockName", CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "main" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "TheOverSwitchMockName", 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, + CONF_USE_MAIN_CENTRAL_CONFIG: True, }, ) @@ -330,6 +441,14 @@ async def test_user_config_flow_window_auto_ko( assert result["step_id"] == "tpi" assert result["errors"] == {} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False} + ) + + 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 ) @@ -339,7 +458,7 @@ async def test_user_config_flow_window_auto_ko( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_PRESETS_CONFIG + result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -348,15 +467,27 @@ async def test_user_config_flow_window_auto_ko( result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=MOCK_WINDOW_AUTO_CONFIG | MOCK_WINDOW_CONFIG, + user_input={ + CONF_WINDOW_SENSOR: "binary_sensor.window_sensor", + CONF_USE_WINDOW_CENTRAL_CONFIG: False, + }, + ) + + 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 # We should stay on window with an error - assert result["step_id"] == "window" assert result["errors"] == { "window_sensor_entity_id": "window_open_detection_method" } + assert result["step_id"] == "window" @pytest.mark.parametrize("expected_lingering_tasks", [True]) @@ -368,19 +499,22 @@ async def test_user_config_flow_over_4_switches( ): """Test the config flow with 4 switchs thermostat_over_switch features""" - SOURCE_CONFIG = { # pylint: disable=wildcard-import, invalid-name - CONF_NAME: "TheOver4SwitchMockName", + await create_central_config(hass) + + SOURCE_CONFIG = { CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, + } + + MAIN_CONFIG = { # pylint: disable=wildcard-import, invalid-name + CONF_NAME: "TheOver4SwitchMockName", 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: False, CONF_USE_MOTION_FEATURE: False, CONF_USE_POWER_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False, + CONF_USE_MAIN_CENTRAL_CONFIG: True, } TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name @@ -390,6 +524,7 @@ async def test_user_config_flow_over_4_switches( CONF_HEATER_4: "switch.mock_switch4", CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_AC_MODE: False, + CONF_INVERSE_SWITCH: False, } result = await hass.config_entries.flow.async_init( @@ -404,6 +539,15 @@ async def test_user_config_flow_over_4_switches( user_input=SOURCE_CONFIG, ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "main" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MAIN_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "type" assert result["errors"] == {} @@ -418,7 +562,7 @@ async def test_user_config_flow_over_4_switches( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG + result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -426,7 +570,7 @@ async def test_user_config_flow_over_4_switches( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_PRESETS_CONFIG + result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -434,15 +578,25 @@ async def test_user_config_flow_over_4_switches( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_ADVANCED_CONFIG + result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result[ - "data" - ] == SOURCE_CONFIG | TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_PRESETS_CONFIG | MOCK_ADVANCED_CONFIG | { - CONF_INVERSE_SWITCH: False - } + assert result["data"] == ( + SOURCE_CONFIG + | MAIN_CONFIG + | TYPE_CONFIG + | { + CONF_USE_MAIN_CENTRAL_CONFIG: True, + CONF_USE_TPI_CENTRAL_CONFIG: True, + CONF_USE_PRESETS_CENTRAL_CONFIG: True, + CONF_USE_WINDOW_CENTRAL_CONFIG: False, + CONF_USE_MOTION_CENTRAL_CONFIG: False, + CONF_USE_POWER_CENTRAL_CONFIG: False, + CONF_USE_PRESENCE_CENTRAL_CONFIG: False, + CONF_USE_ADVANCED_CENTRAL_CONFIG: True, + } + ) assert result["result"] assert result["result"].domain == DOMAIN assert result["result"].version == 1 diff --git a/tests/test_inverted_switch.py b/tests/test_inverted_switch.py index 48aa0e9..b0b8834 100644 --- a/tests/test_inverted_switch.py +++ b/tests/test_inverted_switch.py @@ -1,15 +1,18 @@ # pylint: disable=unused-argument, line-too-long, protected-access """ Test the Window management """ -import asyncio +# import asyncio import logging from unittest.mock import patch, call from datetime import datetime, timedelta -from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch +from custom_components.versatile_thermostat.thermostat_switch import ( + ThermostatOverSwitch, +) from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import logging.getLogger().setLevel(logging.DEBUG) + @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state): @@ -44,14 +47,14 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state): 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 - CONF_INVERSE_SWITCH: True + CONF_INVERSE_SWITCH: True, }, ) with patch( "homeassistant.core.ServiceRegistry.async_call" ) as mock_service_call, patch( - "homeassistant.core.StateMachine.is_state", return_value=True # switch is On + "homeassistant.core.StateMachine.is_state", return_value=True # switch is On ): entity: ThermostatOverSwitch = await create_thermostat( hass, entry, "climate.theoverswitchmockname" @@ -80,7 +83,7 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state): ), patch( "homeassistant.core.ServiceRegistry.async_call" ) as mock_service_call, patch( - "homeassistant.core.StateMachine.is_state", return_value=True # switch is Off + "homeassistant.core.StateMachine.is_state", return_value=True # switch is Off ): event_timestamp = now - timedelta(minutes=4) await send_temperature_change_event(entity, 19, event_timestamp) @@ -90,9 +93,13 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state): # not updated cause mocked assert entity.is_device_active is True assert mock_service_call.call_count == 1 - mock_service_call.assert_has_calls([ - call.async_call('switch', SERVICE_TURN_OFF, {'entity_id': 'switch.mock_switch'}), - ]) + mock_service_call.assert_has_calls( + [ + call.async_call( + "switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"} + ), + ] + ) # 2. Make the temperature up to deactivate the switch with patch( @@ -100,7 +107,8 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state): ), patch( "homeassistant.core.ServiceRegistry.async_call" ) as mock_service_call, patch( - "homeassistant.core.StateMachine.is_state", return_value=False # switch is On -> it should turned off + "homeassistant.core.StateMachine.is_state", + return_value=False, # switch is On -> it should turned off ): event_timestamp = now - timedelta(minutes=3) await send_temperature_change_event(entity, 25, event_timestamp) @@ -114,11 +122,13 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state): await entity._underlyings[0].turn_off() assert mock_service_call.call_count == 1 - mock_service_call.assert_has_calls([ - call.async_call('switch', SERVICE_TURN_ON, {'entity_id': 'switch.mock_switch'}), - ]) - - + mock_service_call.assert_has_calls( + [ + call.async_call( + "switch", SERVICE_TURN_ON, {"entity_id": "switch.mock_switch"} + ), + ] + ) # Clean the entity entity.remove_thermostat()