From ae9b065387634d8756e64dabd3918fa8f0f9ecf5 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Tue, 5 Nov 2024 18:40:14 +0000 Subject: [PATCH 1/4] Add logs to diagnose the case --- .../versatile_thermostat/base_thermostat.py | 32 +++++++++++++++++++ .../versatile_thermostat/commons.py | 1 - .../versatile_thermostat/const.py | 6 +++- .../thermostat_climate.py | 17 +++++++++- .../versatile_thermostat/thermostat_switch.py | 14 +++++++- .../versatile_thermostat/thermostat_valve.py | 14 +++++++- 6 files changed, 79 insertions(+), 5 deletions(-) diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 7342a12..04527c5 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -84,6 +84,10 @@ def get_tz(hass: HomeAssistant): return dt_util.get_time_zone(hass.config.time_zone) +_LOGGER_ENERGY = logging.getLogger( + "custom_components.versatile_thermostat.energy_debug" +) + class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): """Representation of a base class for all Versatile Thermostat device.""" @@ -198,6 +202,7 @@ def __init__( self._attr_translation_key = "versatile_thermostat" self._total_energy = None + _LOGGER_ENERGY.debug("%s - _init_ resetting energy to None", self) # because energy of climate is calculated in the thermostat we have to keep that here and not in underlying entity self._underlying_climate_start_hvac_action_date = None @@ -470,6 +475,7 @@ def post_init(self, config_entry: ConfigData): self._presence_state = None self._total_energy = None + _LOGGER_ENERGY.debug("%s - post_init_ resetting energy to None", self) # Read the parameter from configuration.yaml if it exists short_ema_params = DEFAULT_SHORT_EMA_PARAMS @@ -804,6 +810,11 @@ async def get_my_previous_state(self): old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY) self._total_energy = old_total_energy if old_total_energy is not None else 0 + _LOGGER_ENERGY.debug( + "%s - get_my_previous_state restored energy is %s", + self, + self._total_energy, + ) self.restore_specific_previous_state(old_state) else: @@ -817,6 +828,11 @@ async def get_my_previous_state(self): "No previously saved temperature, setting to %s", self._target_temp ) self._total_energy = 0 + _LOGGER_ENERGY.debug( + "%s - get_my_previous_state no previous state energy is %s", + self, + self._total_energy, + ) if not self._hvac_mode: self._hvac_mode = HVACMode.OFF @@ -2622,6 +2638,22 @@ def update_custom_attributes(self): "hvac_off_reason": self.hvac_off_reason, } + _LOGGER_ENERGY.debug( + "%s - update_custom_attributes saved energy is %s", + self, + self.total_energy, + ) + + @overrides + def async_write_ha_state(self): + """overrides to have log""" + _LOGGER_ENERGY.debug( + "%s - async_write_ha_state written state energy is %s", + self, + self._total_energy, + ) + return super().async_write_ha_state() + @callback def async_registry_entry_updated(self): """update the entity if the config entry have been updated diff --git a/custom_components/versatile_thermostat/commons.py b/custom_components/versatile_thermostat/commons.py index 19a02b3..b76267c 100644 --- a/custom_components/versatile_thermostat/commons.py +++ b/custom_components/versatile_thermostat/commons.py @@ -17,7 +17,6 @@ _LOGGER = logging.getLogger(__name__) - def get_tz(hass: HomeAssistant): """Get the current timezone""" diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index 44f4838..d6ceb7b 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -354,7 +354,11 @@ CONF_WINDOW_ECO_TEMP, ] -SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON +SUPPORT_FLAGS = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON +) SERVICE_SET_PRESENCE = "set_presence" SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature" diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 1411f27..f7ef9ed 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -31,6 +31,10 @@ ) _LOGGER = logging.getLogger(__name__) +_LOGGER_ENERGY = logging.getLogger( + "custom_components.versatile_thermostat.energy_debug" +) + HVAC_ACTION_ON = [ # pylint: disable=invalid-name HVACAction.COOLING, @@ -97,7 +101,7 @@ def post_init(self, config_entry: ConfigData): """Initialize the Thermostat""" super().post_init(config_entry) - + for climate in config_entry.get(CONF_UNDERLYING_LIST): self._underlyings.append( UnderlyingClimate( @@ -549,6 +553,7 @@ def update_custom_attributes(self): ] = self._auto_start_stop_algo.accumulated_error_threshold self.async_write_ha_state() + _LOGGER.debug( "%s - Calling update_custom_attributes: %s", self, @@ -595,8 +600,18 @@ def incremente_energy(self): if self._total_energy is None: self._total_energy = added_energy + _LOGGER_ENERGY.debug( + "%s - incremente_energy set energy is %s", + self, + self._total_energy, + ) else: self._total_energy += added_energy + _LOGGER_ENERGY.debug( + "%s - incremente_energy incremented energy is %s", + self, + self._total_energy, + ) _LOGGER.debug( "%s - added energy is %.3f . Total energy is now: %.3f", diff --git a/custom_components/versatile_thermostat/thermostat_switch.py b/custom_components/versatile_thermostat/thermostat_switch.py index 7869d2a..4e07b8d 100644 --- a/custom_components/versatile_thermostat/thermostat_switch.py +++ b/custom_components/versatile_thermostat/thermostat_switch.py @@ -21,7 +21,9 @@ from .prop_algorithm import PropAlgorithm _LOGGER = logging.getLogger(__name__) - +_LOGGER_ENERGY = logging.getLogger( + "custom_components.versatile_thermostat.energy_debug" +) class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]): """Representation of a base class for a Versatile Thermostat over a switch.""" @@ -183,8 +185,18 @@ def incremente_energy(self): if self._total_energy is None: self._total_energy = added_energy + _LOGGER_ENERGY.debug( + "%s - incremente_energy set energy is %s", + self, + self._total_energy, + ) else: self._total_energy += added_energy + _LOGGER_ENERGY.debug( + "%s - incremente_energy increment energy is %s", + self, + self._total_energy, + ) self.update_custom_attributes() diff --git a/custom_components/versatile_thermostat/thermostat_valve.py b/custom_components/versatile_thermostat/thermostat_valve.py index 7eb7f23..d9324d0 100644 --- a/custom_components/versatile_thermostat/thermostat_valve.py +++ b/custom_components/versatile_thermostat/thermostat_valve.py @@ -25,7 +25,9 @@ from .underlyings import UnderlyingValve _LOGGER = logging.getLogger(__name__) - +_LOGGER_ENERGY = logging.getLogger( + "custom_components.versatile_thermostat.energy_debug" +) class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method """Representation of a class for a Versatile Thermostat over a Valve""" @@ -265,8 +267,18 @@ def incremente_energy(self): if self._total_energy is None: self._total_energy = added_energy + _LOGGER_ENERGY.debug( + "%s - incremente_energy set energy is %s", + self, + self._total_energy, + ) else: self._total_energy += added_energy + _LOGGER_ENERGY.debug( + "%s - get_my_previous_state increment energy is %s", + self, + self._total_energy, + ) self.update_custom_attributes() From c2f53a82326f0563762bf417f443ce61dcff6180 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Tue, 5 Nov 2024 22:39:26 +0100 Subject: [PATCH 2/4] Issue #552 (#608) Co-authored-by: Jean-Marc Collin --- .../versatile_thermostat/config_flow.py | 36 +- tests/test_config_flow.py | 988 +++++++++++------- 2 files changed, 654 insertions(+), 370 deletions(-) diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index 1601a9c..de67f6b 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -109,17 +109,17 @@ def _init_feature_flags(self, _): or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None ) self._infos[CONF_USE_MOTION_FEATURE] = self._infos.get( - CONF_USE_MOTION_FEATURE + CONF_USE_MOTION_FEATURE, False ) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config) self._infos[CONF_USE_POWER_FEATURE] = self._infos.get( - CONF_USE_POWER_CENTRAL_CONFIG + CONF_USE_POWER_CENTRAL_CONFIG, False ) or ( self._infos.get(CONF_POWER_SENSOR) is not None and self._infos.get(CONF_MAX_POWER_SENSOR) is not None ) self._infos[CONF_USE_PRESENCE_FEATURE] = ( - self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG) + self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False) or self._infos.get(CONF_PRESENCE_SENSOR) is not None ) @@ -129,7 +129,7 @@ def _init_feature_flags(self, _): ) self._infos[CONF_USE_AUTO_START_STOP_FEATURE] = ( - self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE) is True + self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False) is True and self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE ) @@ -145,12 +145,17 @@ def _init_central_config_flags(self, infos): CONF_USE_PRESETS_CENTRAL_CONFIG, CONF_USE_PRESENCE_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG, + CONF_USE_CENTRAL_MODE, ): if not is_empty: current_config = self._infos.get(config, None) - self._infos[config] = current_config is True or ( - current_config is None and self._central_config is not None + + self._infos[config] = self._central_config is not None and ( + current_config is True or current_config is None ) + # self._infos[config] = current_config is True or ( + # current_config is None and self._central_config is not None + # ) else: self._infos[config] = self._central_config is not None @@ -209,6 +214,9 @@ async def validate_input(self, data: dict) -> None: CONF_USE_PRESENCE_CENTRAL_CONFIG, CONF_USE_PRESETS_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG, + CONF_USE_CENTRAL_MODE, + CONF_USE_CENTRAL_BOILER_FEATURE, + CONF_USED_BY_CENTRAL_BOILER, ]: if data.get(conf) is True: _LOGGER.error( @@ -306,6 +314,22 @@ def check_config_complete(self, infos) -> bool: ): return False + if ( + infos.get(CONF_PROP_FUNCTION, None) == PROPORTIONAL_FUNCTION_TPI + and infos.get(CONF_USE_TPI_CENTRAL_CONFIG, False) is False + and ( + infos.get(CONF_TPI_COEF_INT, None) is None + or infos.get(CONF_TPI_COEF_EXT) is None + ) + ): + return False + + if ( + infos.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False) is True + and self._central_config is None + ): + return False + return True def merge_user_input(self, data_schema: vol.Schema, user_input: dict): diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index b54d7a1..84c3d3b 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -26,7 +26,7 @@ async def test_show_form(hass: HomeAssistant, init_vtherm_api) -> None: @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) -# Disable this test which don't work anymore (kill the pytest !) +# Disable this test which don't work anymore (kill the pytest !) and make the others tests failed @pytest.mark.skip async def test_user_config_flow_over_switch( hass: HomeAssistant, skip_hass_states_get, init_central_config @@ -314,6 +314,7 @@ async def test_user_config_flow_over_climate( assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER + # 1. Type result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -332,6 +333,7 @@ async def test_user_config_flow_over_climate( ] assert result.get("errors") is None + # 2. Main result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "main"} ) @@ -347,7 +349,7 @@ async def test_user_config_flow_over_climate( CONF_CYCLE_MIN: 5, CONF_DEVICE_POWER: 1, CONF_USE_MAIN_CENTRAL_CONFIG: False, - CONF_USE_CENTRAL_MODE: True, + CONF_USE_CENTRAL_MODE: False, # Keep default values which are False }, ) @@ -355,6 +357,7 @@ async def test_user_config_flow_over_climate( assert result["step_id"] == "main" assert result.get("errors") == {} + # 3. Main 2 result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -369,6 +372,7 @@ async def test_user_config_flow_over_climate( assert result["step_id"] == "menu" assert result.get("errors") is None + # 4. Type result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "type"} ) @@ -401,6 +405,7 @@ async def test_user_config_flow_over_climate( ] assert result.get("errors") is None + # 5. Presets result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "presets"} ) @@ -415,6 +420,7 @@ async def test_user_config_flow_over_climate( assert result["step_id"] == "menu" assert result.get("errors") is None + # 6. Features result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "features"} ) @@ -429,6 +435,7 @@ async def test_user_config_flow_over_climate( CONF_USE_POWER_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False, CONF_USE_WINDOW_FEATURE: False, + CONF_USE_AUTO_START_STOP_FEATURE: False, }, ) assert result["type"] == FlowResultType.MENU @@ -444,6 +451,7 @@ async def test_user_config_flow_over_climate( # "finalize", finalize is not present waiting for advanced configuration ] + # 7. Advanced result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "advanced"} ) @@ -494,7 +502,6 @@ async def test_user_config_flow_over_climate( CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3, } | MOCK_DEFAULT_FEATURE_CONFIG | { CONF_USE_MAIN_CENTRAL_CONFIG: False, - CONF_USE_TPI_CENTRAL_CONFIG: False, CONF_USE_PRESETS_CENTRAL_CONFIG: False, CONF_USE_MOTION_FEATURE: False, CONF_USE_POWER_FEATURE: False, @@ -509,6 +516,7 @@ async def test_user_config_flow_over_climate( CONF_USE_PRESENCE_CENTRAL_CONFIG: False, CONF_USE_ADVANCED_CENTRAL_CONFIG: False, CONF_USED_BY_CENTRAL_BOILER: False, + CONF_USE_CENTRAL_MODE: False, } assert result["result"] assert result["result"].domain == DOMAIN @@ -517,365 +525,365 @@ async def test_user_config_flow_over_climate( assert isinstance(result["result"], ConfigEntry) -@pytest.mark.parametrize("expected_lingering_tasks", [True]) -@pytest.mark.parametrize("expected_lingering_timers", [True]) -# TODO reimplement this -@pytest.mark.skip -async def test_user_config_flow_window_auto_ok( - hass: HomeAssistant, - skip_hass_states_get, - 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} - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == SOURCE_USER - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, - }, - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "main" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - 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_MAIN_CENTRAL_CONFIG: True, - CONF_USED_BY_CENTRAL_BOILER: True, - }, - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "type" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "tpi" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False} - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "tpi" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "presets" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "window" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_USE_WINDOW_CENTRAL_CONFIG: False}, - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "window" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=MOCK_WINDOW_AUTO_CONFIG, - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "advanced" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - 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 - CONF_USE_CENTRAL_MODE: True, # True is the defaulf value - } | 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, - CONF_USED_BY_CENTRAL_BOILER: True, - } - assert result["result"] - assert result["result"].domain == DOMAIN - assert result["result"].version == 1 - assert result["result"].title == "TheOverSwitchMockName" - assert isinstance(result["result"], ConfigEntry) - - -@pytest.mark.parametrize("expected_lingering_tasks", [True]) -@pytest.mark.parametrize("expected_lingering_timers", [True]) -# TODO reimplement this -@pytest.mark.skip -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} - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == SOURCE_USER - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, - }, - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "main" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - 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_MAIN_CENTRAL_CONFIG: True, - }, - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "type" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "tpi" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False} - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "tpi" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "presets" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "window" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_WINDOW_SENSOR: "binary_sensor.window_sensor", - CONF_USE_WINDOW_CENTRAL_CONFIG: False, - }, - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "window" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=MOCK_WINDOW_DELAY_CONFIG, - ) - - # Since issue #280 we cannot have the error because we only display the - # MOCK_WINDOW_DELAY_CONFIG form if we have a sensor configured - assert result["type"] == FlowResultType.MENU - # We should stay on window with an error - assert result.get("errors") is None - # "window_sensor_entity_id": "window_open_detection_method" - # } - assert result["step_id"] == "advanced" - - -@pytest.mark.parametrize("expected_lingering_tasks", [True]) -@pytest.mark.parametrize("expected_lingering_timers", [True]) -# TODO reimplement this -@pytest.mark.skip -async def test_user_config_flow_over_4_switches( - hass: HomeAssistant, - skip_hass_states_get, - skip_control_heating, # pylint: disable=unused-argument -): - """Test the config flow with 4 switchs thermostat_over_switch features""" - - await create_central_config(hass) - - SOURCE_CONFIG = { # pylint: disable=invalid-name - CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, - } - - MAIN_CONFIG = { # pylint: disable=invalid-name - CONF_NAME: "TheOver4SwitchMockName", - CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", - CONF_CYCLE_MIN: 5, - 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, - CONF_USE_CENTRAL_MODE: False, - CONF_USED_BY_CENTRAL_BOILER: False, - } - - TYPE_CONFIG = { # pylint: disable=invalid-name - CONF_UNDERLYING_LIST: ["switch.mock_switch1", "switch.mock_switch2", "switch.mock_switch3","switch.mock_switch4"], - CONF_HEATER_KEEP_ALIVE: 0, - CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, - CONF_AC_MODE: False, - CONF_INVERSE_SWITCH: False, - } - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == SOURCE_USER - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=SOURCE_CONFIG, - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "main" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=MAIN_CONFIG, - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "type" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=TYPE_CONFIG, - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "tpi" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True} - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "presets" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} - ) - - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "advanced" - assert result.get("errors") is None - - result = await hass.config_entries.flow.async_configure( - 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 - | 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 - assert result["result"].title == "TheOver4SwitchMockName" - assert isinstance(result["result"], ConfigEntry) +# @pytest.mark.parametrize("expected_lingering_tasks", [True]) +# @pytest.mark.parametrize("expected_lingering_timers", [True]) +# # TODO reimplement this +# @pytest.mark.skip +# async def test_user_config_flow_window_auto_ok( +# hass: HomeAssistant, +# skip_hass_states_get, +# 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} +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == SOURCE_USER + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], +# user_input={ +# CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, +# }, +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "main" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], +# user_input={ +# 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_MAIN_CENTRAL_CONFIG: True, +# CONF_USED_BY_CENTRAL_BOILER: True, +# }, +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "type" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "tpi" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False} +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "tpi" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "presets" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "window" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], +# user_input={CONF_USE_WINDOW_CENTRAL_CONFIG: False}, +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "window" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], +# user_input=MOCK_WINDOW_AUTO_CONFIG, +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "advanced" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: True} +# ) + +# assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY +# 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 +# CONF_USE_CENTRAL_MODE: True, # True is the defaulf value +# } | 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, +# CONF_USED_BY_CENTRAL_BOILER: True, +# } +# assert result["result"] +# assert result["result"].domain == DOMAIN +# assert result["result"].version == 1 +# assert result["result"].title == "TheOverSwitchMockName" +# assert isinstance(result["result"], ConfigEntry) + + +# @pytest.mark.parametrize("expected_lingering_tasks", [True]) +# @pytest.mark.parametrize("expected_lingering_timers", [True]) +# # TODO reimplement this +# @pytest.mark.skip +# 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} +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == SOURCE_USER + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], +# user_input={ +# CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, +# }, +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "main" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], +# user_input={ +# 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_MAIN_CENTRAL_CONFIG: True, +# }, +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "type" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "tpi" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False} +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "tpi" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "presets" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "window" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], +# user_input={ +# CONF_WINDOW_SENSOR: "binary_sensor.window_sensor", +# CONF_USE_WINDOW_CENTRAL_CONFIG: False, +# }, +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "window" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], +# user_input=MOCK_WINDOW_DELAY_CONFIG, +# ) + +# # Since issue #280 we cannot have the error because we only display the +# # MOCK_WINDOW_DELAY_CONFIG form if we have a sensor configured +# assert result["type"] == FlowResultType.MENU +# # We should stay on window with an error +# assert result.get("errors") is None +# # "window_sensor_entity_id": "window_open_detection_method" +# # } +# assert result["step_id"] == "advanced" + + +# @pytest.mark.parametrize("expected_lingering_tasks", [True]) +# @pytest.mark.parametrize("expected_lingering_timers", [True]) +# # TODO reimplement this +# @pytest.mark.skip +# async def test_user_config_flow_over_4_switches( +# hass: HomeAssistant, +# skip_hass_states_get, +# skip_control_heating, # pylint: disable=unused-argument +# ): +# """Test the config flow with 4 switchs thermostat_over_switch features""" + +# await create_central_config(hass) + +# SOURCE_CONFIG = { # pylint: disable=invalid-name +# CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, +# } + +# MAIN_CONFIG = { # pylint: disable=invalid-name +# CONF_NAME: "TheOver4SwitchMockName", +# CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", +# CONF_CYCLE_MIN: 5, +# 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, +# CONF_USE_CENTRAL_MODE: False, +# CONF_USED_BY_CENTRAL_BOILER: False, +# } + +# TYPE_CONFIG = { # pylint: disable=invalid-name +# CONF_UNDERLYING_LIST: ["switch.mock_switch1", "switch.mock_switch2", "switch.mock_switch3","switch.mock_switch4"], +# CONF_HEATER_KEEP_ALIVE: 0, +# CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, +# CONF_AC_MODE: False, +# CONF_INVERSE_SWITCH: False, +# } + +# result = await hass.config_entries.flow.async_init( +# DOMAIN, context={"source": SOURCE_USER} +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == SOURCE_USER + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], +# user_input=SOURCE_CONFIG, +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "main" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], +# user_input=MAIN_CONFIG, +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "type" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], +# user_input=TYPE_CONFIG, +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "tpi" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: True} +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "presets" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: True} +# ) + +# assert result["type"] == FlowResultType.MENU +# assert result["step_id"] == "advanced" +# assert result.get("errors") is None + +# result = await hass.config_entries.flow.async_configure( +# 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 +# | 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 +# assert result["result"].title == "TheOver4SwitchMockName" +# assert isinstance(result["result"], ConfigEntry) @pytest.mark.parametrize("expected_lingering_tasks", [True]) @@ -979,7 +987,7 @@ async def test_user_config_flow_over_climate_auto_start_stop( CONF_CYCLE_MIN: 5, CONF_DEVICE_POWER: 1, CONF_USE_MAIN_CENTRAL_CONFIG: False, - CONF_USE_CENTRAL_MODE: True, + CONF_USE_CENTRAL_MODE: False, # Keep default values which are False }, ) @@ -1109,7 +1117,7 @@ async def test_user_config_flow_over_climate_auto_start_stop( CONF_USE_WINDOW_FEATURE: False, CONF_USE_AUTO_START_STOP_FEATURE: False, CONF_USE_CENTRAL_BOILER_FEATURE: False, - CONF_USE_TPI_CENTRAL_CONFIG: False, + CONF_USE_CENTRAL_MODE: False, CONF_USE_WINDOW_CENTRAL_CONFIG: False, CONF_USE_MOTION_CENTRAL_CONFIG: False, CONF_USE_POWER_CENTRAL_CONFIG: False, @@ -1124,3 +1132,255 @@ async def test_user_config_flow_over_climate_auto_start_stop( assert result["result"].version == 2 assert result["result"].title == "TheOverClimateMockName" assert isinstance(result["result"], ConfigEntry) + + +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_user_config_flow_over_switch_bug_552_tpi( + hass: HomeAssistant, skip_hass_states_get +): # pylint: disable=unused-argument + """Test the bug 552 - a VTherm over_switch can be configured without TPI parameters + if 'use central config is checked with no central config""" + + # 1. Thermostat over_switch + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == SOURCE_USER + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, + }, + ) + + # 2. Menu + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "menu" + assert result["menu_options"] == [ + "main", + "features", + "type", + "presets", + "advanced", + "configuration_not_complete", + ] + assert result.get("errors") is None + + # 3. Main attributes + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "main"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "main" + assert result.get("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_CYCLE_MIN: 5, + CONF_DEVICE_POWER: 1, + CONF_USE_MAIN_CENTRAL_CONFIG: False, + CONF_USE_CENTRAL_MODE: False, + }, + ) + + # 4. Main attributes 2 + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "main" + assert result.get("errors") == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", + CONF_TEMP_MIN: 15, + CONF_TEMP_MAX: 30, + CONF_STEP_TEMPERATURE: 0.5, + # Keep default values which are False + }, + ) + + # 5. Menu + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "menu" + assert result.get("errors") is None + assert result["menu_options"] == [ + "main", + "features", + "type", + "presets", + "advanced", + "configuration_not_complete", # tpi and presets are not configured and there is no central configuration + ] + + # 6. Type + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "type"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "type" + assert result.get("errors") == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_UNDERLYING_LIST: ["switch.mock_switch"], + CONF_HEATER_KEEP_ALIVE: 0, + CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, + CONF_AC_MODE: False, + CONF_INVERSE_SWITCH: False, + }, + ) + + # 7. Menu + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "menu" + assert result.get("errors") is None + assert result["menu_options"] == [ + "main", + "features", + "type", + "tpi", + "presets", + "advanced", + "configuration_not_complete", # advanced, tpi and presets are not configured and there is no central configuration + ] + + # 8. Advanced + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "advanced"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "advanced" + assert result.get("errors") == {} + + # 8. Advanced 2 + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: False}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "advanced" + assert result.get("errors") == {} + + # 9. Advanced 3 + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_MINIMAL_ACTIVATION_DELAY: 10, + CONF_SECURITY_DELAY_MIN: 5, + CONF_SECURITY_MIN_ON_PERCENT: 0.4, + CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3, + }, + ) + + # 10. Menu + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "menu" + assert result.get("errors") is None + assert result["menu_options"] == [ + "main", + "features", + "type", + "tpi", + "presets", + "advanced", + "configuration_not_complete", # tpi is not configured and there is no central configuration + ] + + # 11. TPI + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "tpi"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "tpi" + assert result.get("errors") == {} + + # 11. TPI 2 + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "tpi" + assert result.get("errors") == {} + + # 12. Menu + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG + ) + + # 11. Presets + # We do not configure preset so we should have a default: don't use preset central config + # result = await hass.config_entries.flow.async_configure( + # result["flow_id"], user_input={"next_step_id": "presets"} + # ) + # assert result["type"] == FlowResultType.FORM + # assert result["step_id"] == "presets" + # assert result.get("errors") == {} + # + # result = await hass.config_entries.flow.async_configure( + # result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: False} + # ) + + # 12. Menu + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "menu" + assert result.get("errors") is None + assert result["menu_options"] == [ + "main", + "features", + "type", + "tpi", + "presets", + "advanced", + "finalize", # all is now configured + ] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "finalize"} + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result.get("errors") is None + 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 + | { + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", + CONF_TEMP_MIN: 15, + CONF_TEMP_MAX: 30, + CONF_STEP_TEMPERATURE: 0.5, + CONF_MINIMAL_ACTIVATION_DELAY: 10, + CONF_SECURITY_DELAY_MIN: 5, + CONF_SECURITY_MIN_ON_PERCENT: 0.4, + CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3, + 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, + CONF_USE_AUTO_START_STOP_FEATURE: False, + CONF_USE_CENTRAL_MODE: False, + CONF_USED_BY_CENTRAL_BOILER: False, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_USE_CENTRAL_BOILER_FEATURE: False, + } + ) + assert result["result"] + assert result["result"].domain == DOMAIN + assert result["result"].version == 2 + assert result["result"].title == "TheOverSwitchMockName" + assert isinstance(result["result"], ConfigEntry) From 75dd2be2b1d7fdf0164490ba8b250bc8e5847619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Tue, 5 Nov 2024 22:47:42 +0100 Subject: [PATCH 3/4] Fix typo (#607) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af7f793..2e39a5c 100644 --- a/README.md +++ b/README.md @@ -1353,7 +1353,7 @@ Example of graph obtained with Plotly : ## And always better and better with the NOTIFIER daemon app to notify events -This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https ://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats. +This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats. This is a great example of using the notifications described here [notification](#notifications). From 4a97a2faf2e0f24c956edd343c73ec51d968945d Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Wed, 6 Nov 2024 19:05:38 +0000 Subject: [PATCH 4/4] - Force writing state when entity is removed - Fix bug with issue #552 on CONF_USE_CENTRAL_BOILER_FEATURE which should be proposed on a central configuration - Improve reload of entity to avoid reloading all VTherm. Only the reconfigured one will be reloaded --- .../versatile_thermostat/__init__.py | 11 +++++++++-- .../versatile_thermostat/base_thermostat.py | 17 +++++++++++++++-- .../versatile_thermostat/config_flow.py | 2 +- .../versatile_thermostat/vtherm_api.py | 6 ++++-- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/custom_components/versatile_thermostat/__init__.py b/custom_components/versatile_thermostat/__init__.py index 5c7c366..c95209d 100644 --- a/custom_components/versatile_thermostat/__init__.py +++ b/custom_components/versatile_thermostat/__init__.py @@ -178,13 +178,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if hass.state == CoreState.running: await api.reload_central_boiler_entities_list() - await api.init_vtherm_links() + await api.init_vtherm_links(entry.entry_id) return True async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener.""" + + _LOGGER.debug( + "Calling update_listener entry: entry_id='%s', value='%s'", + entry.entry_id, + entry.data, + ) + if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG: await reload_all_vtherm(hass) else: @@ -193,7 +200,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) if api is not None: await api.reload_central_boiler_entities_list() - await api.init_vtherm_links() + await api.init_vtherm_links(entry.entry_id) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 04527c5..0b8cc9c 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -19,7 +19,10 @@ ) from homeassistant.components.climate import ClimateEntity -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.restore_state import ( + RestoreEntity, + async_get as restore_async_get, +) from homeassistant.helpers.entity import Entity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType @@ -591,14 +594,24 @@ async def async_added_to_hass(self): # issue 428. Link to others entities will start at link # await self.async_startup() + async def async_will_remove_from_hass(self): + """Try to force backup of entity""" + _LOGGER_ENERGY.debug( + "%s - force write before remove. Energy is %s", self, self.total_energy + ) + # Force dump in background + await restore_async_get(self.hass).async_dump_states() + def remove_thermostat(self): """Called when the thermostat will be removed""" _LOGGER.info("%s - Removing thermostat", self) + for under in self._underlyings: under.remove_entity() async def async_startup(self, central_configuration): - """Triggered on startup, used to get old state and set internal states accordingly""" + """Triggered on startup, used to get old state and set internal states accordingly. This is triggered by + VTherm API""" _LOGGER.debug("%s - Calling async_startup", self) _LOGGER.debug("%s - Calling async_startup_internal", self) diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index de67f6b..5c0a82a 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -215,7 +215,7 @@ async def validate_input(self, data: dict) -> None: CONF_USE_PRESETS_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG, CONF_USE_CENTRAL_MODE, - CONF_USE_CENTRAL_BOILER_FEATURE, + # CONF_USE_CENTRAL_BOILER_FEATURE, this is for Central Config CONF_USED_BY_CENTRAL_BOILER, ]: if data.get(conf) is True: diff --git a/custom_components/versatile_thermostat/vtherm_api.py b/custom_components/versatile_thermostat/vtherm_api.py index 52a0609..98cef53 100644 --- a/custom_components/versatile_thermostat/vtherm_api.py +++ b/custom_components/versatile_thermostat/vtherm_api.py @@ -150,10 +150,11 @@ def get_temperature_number_value(self, config_id, preset_name) -> float | None: return entity.state return None - async def init_vtherm_links(self): + async def init_vtherm_links(self, entry_id=None): """Initialize all VTherms entities links This method is called when HA is fully started (and all entities should be initialized) Or when we need to reload all VTherm links (with Number temp entities, central boiler, ...) + If entry_id is set, only the VTherm of this entry will be reloaded """ await self.reload_central_boiler_binary_listener() await self.reload_central_boiler_entities_list() @@ -175,7 +176,8 @@ async def init_vtherm_links(self): entity.device_info and entity.device_info.get("model", None) == DOMAIN ): - await entity.async_startup(self.find_central_configuration()) + if entry_id is None or entry_id == entity.unique_id: + await entity.async_startup(self.find_central_configuration()) async def init_vtherm_preset_with_central(self): """Init all VTherm presets when the VTherm uses central temperature"""