From c0b186b8c1a3690bb8b8049a5354d81ffa736404 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sat, 11 Nov 2023 15:20:52 +0000 Subject: [PATCH] Issue #181 - auto-window for over_climate doesn't work --- .../versatile_thermostat/base_thermostat.py | 4 +- .../versatile_thermostat/sensor.py | 23 ++++++- tests/test_window.py | 62 ++++++++----------- 3 files changed, 48 insertions(+), 41 deletions(-) diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 8224c3b..51c93d8 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -1723,8 +1723,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity): and self.hvac_mode != HVACMode.OFF ): if ( - not self.proportional_algorithm - or self.proportional_algorithm.on_percent <= 0.0 + self.proportional_algorithm + and self.proportional_algorithm.on_percent <= 0.0 ): _LOGGER.info( "%s - Start auto detection of open window slope=%.3f but no heating detected (on_percent<=0). Forget the event", diff --git a/custom_components/versatile_thermostat/sensor.py b/custom_components/versatile_thermostat/sensor.py index 0f0d1a1..f02ebdf 100644 --- a/custom_components/versatile_thermostat/sensor.py +++ b/custom_components/versatile_thermostat/sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorDeviceClass, SensorStateClass, - UnitOfTemperature + UnitOfTemperature, ) from homeassistant.config_entries import ConfigEntry @@ -54,7 +54,10 @@ async def async_setup_entry( ] if entry.data.get(CONF_DEVICE_POWER): entities.append(EnergySensor(hass, unique_id, name, entry.data)) - if entry.data.get(CONF_THERMOSTAT_TYPE) in [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_VALVE]: + if entry.data.get(CONF_THERMOSTAT_TYPE) in [ + CONF_THERMOSTAT_SWITCH, + CONF_THERMOSTAT_VALVE, + ]: entities.append(MeanPowerSensor(hass, unique_id, name, entry.data)) if entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI: @@ -202,6 +205,9 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity): if self.my_climate and self.my_climate.proportional_algorithm else None ) + if on_percent is None: + return + if math.isnan(on_percent) or math.isinf(on_percent): raise ValueError(f"Sensor has illegal state {on_percent}") @@ -234,6 +240,7 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity): """Return the suggested number of decimal digits for display.""" return 1 + class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity): """Representation of a on percent sensor which exposes the on_percent in a cycle""" @@ -295,6 +302,10 @@ class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity): if self.my_climate and self.my_climate.proportional_algorithm else None ) + + if on_time is None: + return + if math.isnan(on_time) or math.isinf(on_time): raise ValueError(f"Sensor has illegal state {on_time}") @@ -340,6 +351,9 @@ class OffTimeSensor(VersatileThermostatBaseEntity, SensorEntity): if self.my_climate and self.my_climate.proportional_algorithm else None ) + if off_time is None: + return + if math.isnan(off_time) or math.isinf(off_time): raise ValueError(f"Sensor has illegal state {off_time}") @@ -476,6 +490,7 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity): """Return the suggested number of decimal digits for display.""" return 2 + class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity): """Representation of a Energy sensor which exposes the energy""" @@ -493,7 +508,9 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity): if math.isnan(self.my_climate.regulated_target_temp) or math.isinf( self.my_climate.regulated_target_temp ): - raise ValueError(f"Sensor has illegal state {self.my_climate.regulated_target_temp}") + raise ValueError( + f"Sensor has illegal state {self.my_climate.regulated_target_temp}" + ) old_state = self._attr_native_value self._attr_native_value = round( diff --git a/tests/test_window.py b/tests/test_window.py index fd87722..c651eae 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -242,7 +242,7 @@ async def test_window_management_time_enough( @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state): - """Test the Power management""" + """Test the Window management""" entry = MockConfigEntry( domain=DOMAIN, @@ -430,11 +430,11 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st entry = MockConfigEntry( domain=DOMAIN, - title="TheOverSwitchMockName", + title="TheOverClimateMockName", unique_id="uniqueId", data={ - CONF_NAME: "TheOverSwitchMockName", - CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, + CONF_NAME: "TheOverClimateMockName", + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE, CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", CONF_CYCLE_MIN: 5, @@ -447,10 +447,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st 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_CLIMATE: "switch.mock_climate", CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_MIN_ON_PERCENT: 0.3, @@ -461,7 +458,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st ) entity: BaseThermostat = await create_thermostat( - hass, entry, "climate.theoverswitchmockname" + hass, entry, "climate.theoverclimatemockname" ) assert entity @@ -469,7 +466,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st now = datetime.now(tz) tpi_algo = entity._prop_algorithm - assert tpi_algo + assert tpi_algo is None await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_preset_mode(PRESET_BOOST) @@ -484,18 +481,16 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" + ) as mock_set_hvac_mode, patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.is_device_active", return_value=True, ): event_timestamp = now - timedelta(minutes=4) await send_temperature_change_event(entity, 19, event_timestamp) - # The heater turns on - assert mock_heater_on.call_count == 1 + # The climate turns on but was alredy on + assert mock_set_hvac_mode.call_count == 0 assert entity.last_temperature_slope is None assert entity._window_auto_algo.is_window_open_detected() is False assert entity._window_auto_algo.is_window_close_detected() is False @@ -505,10 +500,8 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" + ) as mock_set_hvac_mode, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", return_value=True, ): @@ -531,8 +524,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st ], any_order=True, ) - assert mock_heater_on.call_count == 0 - assert mock_heater_off.call_count >= 1 + assert mock_set_hvac_mode.call_count >= 1 assert entity.last_temperature_slope == -1 assert entity._window_auto_algo.is_window_open_detected() is True assert entity._window_auto_algo.is_window_close_detected() is False @@ -543,17 +535,14 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" + ) as mock_set_hvac_mode, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", return_value=False, ): await asyncio.sleep(0.3) - assert mock_heater_on.call_count == 1 - assert mock_heater_off.call_count == 0 + assert mock_set_hvac_mode.call_count == 1 assert round(entity.last_temperature_slope, 3) == -1 # Because the algorithm is not aware of the expiration, for the algo we are still in alert assert entity._window_auto_algo.is_window_open_detected() is True @@ -674,12 +663,11 @@ async def test_window_auto_no_on_percent( # Clean the entity entity.remove_thermostat() -#PR - Adding Window Bypass + +# PR - Adding Window Bypass @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) -async def test_window_bypass( - hass: HomeAssistant, skip_hass_states_is_state -): +async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state): """Test the Window management when bypass enabled""" entry = MockConfigEntry( @@ -810,7 +798,8 @@ async def test_window_bypass( # Clean the entity entity.remove_thermostat() -#PR - Adding Window bypass for window auto algorithm + +# PR - Adding Window bypass for window auto algorithm @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state): @@ -921,7 +910,8 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state # Clean the entity entity.remove_thermostat() -#PR - Adding Window bypass AFTER detection have been done should reactivate the heater + +# PR - Adding Window bypass AFTER detection have been done should reactivate the heater @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is_state): @@ -1049,4 +1039,4 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is ) # Clean the entity - entity.remove_thermostat() \ No newline at end of file + entity.remove_thermostat()