From 36cab0c91f08da8671b7c6bce4a7e6871ca4f820 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sun, 24 Nov 2024 09:32:19 +0000 Subject: [PATCH] Test multi ok --- .../thermostat_climate.py | 3 +- tests/commons.py | 8 +- tests/test_overclimate_valve.py | 299 ++++++++++++++++++ 3 files changed, 305 insertions(+), 5 deletions(-) diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 5941eed..f5de04e 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -180,7 +180,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): await super()._async_internal_set_temperature(temperature) self._regulation_algo.set_target_temp(self.target_temperature) - await self._send_regulated_temperature(force=True) + # is done by control_heating method. No need to do it here + # await self._send_regulated_temperature(force=True) async def _send_regulated_temperature(self, force=False): """Sends the regulated temperature to all underlying""" diff --git a/tests/commons.py b/tests/commons.py index 060ba71..f98ca62 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -1019,8 +1019,8 @@ default_temperatures_ac_away = { "boost_ac": 23.0, "frost_away": 7.1, "eco_away": 17.1, - "comfort_away": 19.1, - "boost_away": 21.1, + "comfort_away": 17.2, + "boost_away": 17.3, "eco_ac_away": 27.1, "comfort_ac_away": 25.1, "boost_ac_away": 23.1, @@ -1033,8 +1033,8 @@ default_temperatures_away = { "boost": 21.0, "frost_away": 7.1, "eco_away": 17.1, - "comfort_away": 19.1, - "boost_away": 21.1, + "comfort_away": 17.2, + "boost_away": 17.3, } default_temperatures = { diff --git a/tests/test_overclimate_valve.py b/tests/test_overclimate_valve.py index a3b56db..84cbc25 100644 --- a/tests/test_overclimate_valve.py +++ b/tests/test_overclimate_valve.py @@ -293,4 +293,303 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get + await hass.async_block_till_done() + + +async def test_over_climate_valve_multi_presence( + hass: HomeAssistant, skip_hass_states_get +): + """Test the normal full start of a thermostat in thermostat_over_climate type""" + + entry = MockConfigEntry( + domain=DOMAIN, + title="TheOverClimateMockName", + unique_id="uniqueId", + data={ + CONF_NAME: "TheOverClimateMockName", + 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, + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE, + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", + CONF_TEMP_MIN: 15, + CONF_TEMP_MAX: 30, + CONF_STEP_TEMPERATURE: 0.1, + CONF_UNDERLYING_LIST: ["climate.mock_climate1", "climate.mock_climate2"], + CONF_AC_MODE: False, + CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE, + CONF_AUTO_REGULATION_DTEMP: 0.5, + CONF_AUTO_REGULATION_PERIOD_MIN: 2, + CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH, + CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False, + CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, + CONF_TPI_COEF_INT: 0.3, + CONF_TPI_COEF_EXT: 0.1, + CONF_OPENING_DEGREE_LIST: [ + "number.mock_opening_degree1", + "number.mock_opening_degree2", + ], + CONF_CLOSING_DEGREE_LIST: [ + "number.mock_closing_degree1", + "number.mock_closing_degree2", + ], + CONF_OFFSET_CALIBRATION_LIST: [ + "number.mock_offset_calibration1", + "number.mock_offset_calibration2", + ], + CONF_USE_PRESENCE_FEATURE: True, + CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor", + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + } + | MOCK_DEFAULT_CENTRAL_CONFIG + | MOCK_ADVANCED_CONFIG, + ) + + fake_underlying_climate1 = MockClimate( + hass, "mockUniqueId1", "MockClimateName1", {} + ) + fake_underlying_climate2 = MockClimate( + hass, "mockUniqueId2", "MockClimateName2", {} + ) + + # mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list + mock_get_state_side_effect = SideEffects( + { + # Valve 1 is open + "number.mock_opening_degree1": State( + "number.mock_opening_degree1", "10", {"min": 0, "max": 100} + ), + "number.mock_closing_degree1": State( + "number.mock_closing_degree1", "90", {"min": 0, "max": 100} + ), + "number.mock_offset_calibration1": State( + "number.mock_offset_calibration1", "0", {"min": -12, "max": 12} + ), + # Valve 2 is closed + "number.mock_opening_degree2": State( + "number.mock_opening_degree2", "0", {"min": 0, "max": 100} + ), + "number.mock_closing_degree2": State( + "number.mock_closing_degree2", "100", {"min": 0, "max": 100} + ), + "number.mock_offset_calibration2": State( + "number.mock_offset_calibration2", "10", {"min": -12, "max": 12} + ), + }, + State("unknown.entity_id", "unknown"), + ) + + # 1. initialize the VTherm + tz = get_tz(hass) # pylint: disable=invalid-name + now: datetime = datetime.now(tz=tz) + + # fmt: off + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ + patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", side_effect=[fake_underlying_climate1, fake_underlying_climate2]) as mock_find_climate, \ + patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ + patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state: + # fmt: on + + vtherm: ThermostatOverClimateValve = await create_thermostat(hass, entry, "climate.theoverclimatemockname") + assert vtherm + assert isinstance(vtherm, ThermostatOverClimateValve) + + assert vtherm.name == "TheOverClimateMockName" + assert vtherm.is_over_climate is True + assert vtherm.have_valve_regulation is True + + vtherm._set_now(now) + + # initialize the temps + await set_all_climate_preset_temp(hass, vtherm, default_temperatures_away, "theoverclimatemockname") + + await send_temperature_change_event(vtherm, 18, now, True) + await send_ext_temperature_change_event(vtherm, 18, now, True) + await send_presence_change_event(vtherm, False, True, now) + + await vtherm.async_set_preset_mode(PRESET_COMFORT) + await vtherm.async_set_hvac_mode(HVACMode.HEAT) + + assert vtherm.target_temperature == 17.2 + + # fmt: off + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ + patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ + patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state: + # fmt: on + now = now + timedelta(minutes=3) + vtherm._set_now(now) + + await send_presence_change_event(vtherm, True, False, now) + await hass.async_block_till_done() + + assert vtherm.is_device_active is True + assert vtherm.valve_open_percent == 40 + + # the underlying set temperature call and the call to the valve + assert mock_service_call.call_count == 8 + mock_service_call.assert_has_calls( + [ + call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree1'}), + call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree1'}), + call(domain='number', service='set_value', service_data={'value': 3}, target={'entity_id': 'number.mock_offset_calibration1'}), + call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree2'}), + call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree2'}), + call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'}), + call("climate","set_temperature",{ + "entity_id": "climate.mock_climate1", + "temperature": 19, + }, + ), + call("climate","set_temperature",{ + "entity_id": "climate.mock_climate2", + "temperature": 19, + }, + ), + ] + ) + + assert mock_get_state.call_count > 5 # each temp sensor + each valve + + + # 2. Starts heating slowly (18 vs 19) + now = now + timedelta(minutes=1) + vtherm._set_now(now) + + await vtherm.async_set_hvac_mode(HVACMode.HEAT) + # fmt: off + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ + patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ + patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state: + # fmt: on + now = now + timedelta(minutes=2) # avoid temporal filter + vtherm._set_now(now) + + await vtherm.async_set_preset_mode(PRESET_COMFORT) + await hass.async_block_till_done() + + assert vtherm.hvac_mode is HVACMode.HEAT + assert vtherm.preset_mode is PRESET_COMFORT + assert vtherm.target_temperature == 19 + assert vtherm.current_temperature == 18 + assert vtherm.valve_open_percent == 40 # 0.3*1 + 0.1*1 + + + assert mock_service_call.call_count == 4 + mock_service_call.assert_has_calls( + [ + call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate', 'temperature': 19.0}), + call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree'}), + call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree'}), + # 3 = 18 (room) - 15 (current of underlying) + 0 (current offset) + call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration'}) + ] + ) + + # set the opening to 40% + mock_get_state_side_effect.add_or_update_side_effect( + "number.mock_opening_degree", + State( + "number.mock_opening_degree", "40", {"min": 0, "max": 100} + )) + + assert vtherm.hvac_action is HVACAction.HEATING + assert vtherm.is_device_active is True + + # 2. Starts heating very slowly (18.9 vs 19) + now = now + timedelta(minutes=2) + vtherm._set_now(now) + # fmt: off + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ + patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ + patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state: + # fmt: on + # set the offset to 3 + mock_get_state_side_effect.add_or_update_side_effect( + "number.mock_offset_calibration", + State( + "number.mock_offset_calibration", "3", {"min": -12, "max": 12} + )) + + await send_temperature_change_event(vtherm, 18.9, now, True) + await hass.async_block_till_done() + + assert vtherm.hvac_mode is HVACMode.HEAT + assert vtherm.preset_mode is PRESET_COMFORT + assert vtherm.target_temperature == 19 + assert vtherm.current_temperature == 18.9 + assert vtherm.valve_open_percent == 13 # 0.3*0.1 + 0.1*1 + + + assert mock_service_call.call_count == 3 + mock_service_call.assert_has_calls( + [ + call(domain='number', service='set_value', service_data={'value': 13}, target={'entity_id': 'number.mock_opening_degree'}), + call(domain='number', service='set_value', service_data={'value': 87}, target={'entity_id': 'number.mock_closing_degree'}), + # 6 = 18 (room) - 15 (current of underlying) + 3 (current offset) + call(domain='number', service='set_value', service_data={'value': 6.899999999999999}, target={'entity_id': 'number.mock_offset_calibration'}) + ] + ) + + # set the opening to 13% + mock_get_state_side_effect.add_or_update_side_effect( + "number.mock_opening_degree", + State( + "number.mock_opening_degree", "13", {"min": 0, "max": 100} + )) + + assert vtherm.hvac_action is HVACAction.HEATING + assert vtherm.is_device_active is True + + # 3. Stop heating 21 > 19 + now = now + timedelta(minutes=2) + vtherm._set_now(now) + # fmt: off + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ + patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ + patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state: + # fmt: on + # set the offset to 3 + mock_get_state_side_effect.add_or_update_side_effect( + "number.mock_offset_calibration", + State( + "number.mock_offset_calibration", "3", {"min": -12, "max": 12} + )) + + await send_temperature_change_event(vtherm, 21, now, True) + await hass.async_block_till_done() + + assert vtherm.hvac_mode is HVACMode.HEAT + assert vtherm.preset_mode is PRESET_COMFORT + assert vtherm.target_temperature == 19 + assert vtherm.current_temperature == 21 + assert vtherm.valve_open_percent == 0 # 0.3* (-2) + 0.1*1 + + + assert mock_service_call.call_count == 3 + mock_service_call.assert_has_calls( + [ + call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}), + call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}), + # 6 = 18 (room) - 15 (current of underlying) + 3 (current offset) + call(domain='number', service='set_value', service_data={'value': 9.0}, target={'entity_id': 'number.mock_offset_calibration'}) + ] + ) + + # set the opening to 13% + mock_get_state_side_effect.add_or_update_side_effect( + "number.mock_opening_degree", + State( + "number.mock_opening_degree", "0", {"min": 0, "max": 100} + )) + + assert vtherm.hvac_action is HVACAction.OFF + assert vtherm.is_device_active is False + + + await hass.async_block_till_done()