Skip to content

Commit c0b186b

Browse files
author
Jean-Marc Collin
committed
Issue #181 - auto-window for over_climate doesn't work
1 parent 01e761a commit c0b186b

File tree

3 files changed

+48
-41
lines changed

3 files changed

+48
-41
lines changed

custom_components/versatile_thermostat/base_thermostat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,8 +1723,8 @@ async def deactivate_window_auto(auto=False):
17231723
and self.hvac_mode != HVACMode.OFF
17241724
):
17251725
if (
1726-
not self.proportional_algorithm
1727-
or self.proportional_algorithm.on_percent <= 0.0
1726+
self.proportional_algorithm
1727+
and self.proportional_algorithm.on_percent <= 0.0
17281728
):
17291729
_LOGGER.info(
17301730
"%s - Start auto detection of open window slope=%.3f but no heating detected (on_percent<=0). Forget the event",

custom_components/versatile_thermostat/sensor.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
SensorEntity,
1212
SensorDeviceClass,
1313
SensorStateClass,
14-
UnitOfTemperature
14+
UnitOfTemperature,
1515
)
1616
from homeassistant.config_entries import ConfigEntry
1717

@@ -54,7 +54,10 @@ async def async_setup_entry(
5454
]
5555
if entry.data.get(CONF_DEVICE_POWER):
5656
entities.append(EnergySensor(hass, unique_id, name, entry.data))
57-
if entry.data.get(CONF_THERMOSTAT_TYPE) in [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_VALVE]:
57+
if entry.data.get(CONF_THERMOSTAT_TYPE) in [
58+
CONF_THERMOSTAT_SWITCH,
59+
CONF_THERMOSTAT_VALVE,
60+
]:
5861
entities.append(MeanPowerSensor(hass, unique_id, name, entry.data))
5962

6063
if entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI:
@@ -202,6 +205,9 @@ async def async_my_climate_changed(self, event: Event = None):
202205
if self.my_climate and self.my_climate.proportional_algorithm
203206
else None
204207
)
208+
if on_percent is None:
209+
return
210+
205211
if math.isnan(on_percent) or math.isinf(on_percent):
206212
raise ValueError(f"Sensor has illegal state {on_percent}")
207213

@@ -234,6 +240,7 @@ def suggested_display_precision(self) -> int | None:
234240
"""Return the suggested number of decimal digits for display."""
235241
return 1
236242

243+
237244
class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
238245
"""Representation of a on percent sensor which exposes the on_percent in a cycle"""
239246

@@ -295,6 +302,10 @@ async def async_my_climate_changed(self, event: Event = None):
295302
if self.my_climate and self.my_climate.proportional_algorithm
296303
else None
297304
)
305+
306+
if on_time is None:
307+
return
308+
298309
if math.isnan(on_time) or math.isinf(on_time):
299310
raise ValueError(f"Sensor has illegal state {on_time}")
300311

@@ -340,6 +351,9 @@ async def async_my_climate_changed(self, event: Event = None):
340351
if self.my_climate and self.my_climate.proportional_algorithm
341352
else None
342353
)
354+
if off_time is None:
355+
return
356+
343357
if math.isnan(off_time) or math.isinf(off_time):
344358
raise ValueError(f"Sensor has illegal state {off_time}")
345359

@@ -476,6 +490,7 @@ def suggested_display_precision(self) -> int | None:
476490
"""Return the suggested number of decimal digits for display."""
477491
return 2
478492

493+
479494
class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
480495
"""Representation of a Energy sensor which exposes the energy"""
481496

@@ -493,7 +508,9 @@ async def async_my_climate_changed(self, event: Event = None):
493508
if math.isnan(self.my_climate.regulated_target_temp) or math.isinf(
494509
self.my_climate.regulated_target_temp
495510
):
496-
raise ValueError(f"Sensor has illegal state {self.my_climate.regulated_target_temp}")
511+
raise ValueError(
512+
f"Sensor has illegal state {self.my_climate.regulated_target_temp}"
513+
)
497514

498515
old_state = self._attr_native_value
499516
self._attr_native_value = round(

tests/test_window.py

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ async def test_window_management_time_enough(
242242
@pytest.mark.parametrize("expected_lingering_tasks", [True])
243243
@pytest.mark.parametrize("expected_lingering_timers", [True])
244244
async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
245-
"""Test the Power management"""
245+
"""Test the Window management"""
246246

247247
entry = MockConfigEntry(
248248
domain=DOMAIN,
@@ -430,11 +430,11 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
430430

431431
entry = MockConfigEntry(
432432
domain=DOMAIN,
433-
title="TheOverSwitchMockName",
433+
title="TheOverClimateMockName",
434434
unique_id="uniqueId",
435435
data={
436-
CONF_NAME: "TheOverSwitchMockName",
437-
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
436+
CONF_NAME: "TheOverClimateMockName",
437+
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
438438
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
439439
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
440440
CONF_CYCLE_MIN: 5,
@@ -447,10 +447,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
447447
CONF_USE_MOTION_FEATURE: False,
448448
CONF_USE_POWER_FEATURE: False,
449449
CONF_USE_PRESENCE_FEATURE: False,
450-
CONF_HEATER: "switch.mock_switch",
451-
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
452-
CONF_TPI_COEF_INT: 0.3,
453-
CONF_TPI_COEF_EXT: 0.01,
450+
CONF_CLIMATE: "switch.mock_climate",
454451
CONF_MINIMAL_ACTIVATION_DELAY: 30,
455452
CONF_SECURITY_DELAY_MIN: 5,
456453
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
@@ -461,15 +458,15 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
461458
)
462459

463460
entity: BaseThermostat = await create_thermostat(
464-
hass, entry, "climate.theoverswitchmockname"
461+
hass, entry, "climate.theoverclimatemockname"
465462
)
466463
assert entity
467464

468465
tz = get_tz(hass) # pylint: disable=invalid-name
469466
now = datetime.now(tz)
470467

471468
tpi_algo = entity._prop_algorithm
472-
assert tpi_algo
469+
assert tpi_algo is None
473470

474471
await entity.async_set_hvac_mode(HVACMode.HEAT)
475472
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
484481
with patch(
485482
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
486483
) as mock_send_event, patch(
487-
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
488-
) as mock_heater_on, patch(
489-
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
490-
) as mock_heater_off, patch(
491-
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
484+
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
485+
) as mock_set_hvac_mode, patch(
486+
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.is_device_active",
492487
return_value=True,
493488
):
494489
event_timestamp = now - timedelta(minutes=4)
495490
await send_temperature_change_event(entity, 19, event_timestamp)
496491

497-
# The heater turns on
498-
assert mock_heater_on.call_count == 1
492+
# The climate turns on but was alredy on
493+
assert mock_set_hvac_mode.call_count == 0
499494
assert entity.last_temperature_slope is None
500495
assert entity._window_auto_algo.is_window_open_detected() is False
501496
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
505500
with patch(
506501
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
507502
) as mock_send_event, patch(
508-
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
509-
) as mock_heater_on, patch(
510-
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
511-
) as mock_heater_off, patch(
503+
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
504+
) as mock_set_hvac_mode, patch(
512505
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
513506
return_value=True,
514507
):
@@ -531,8 +524,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
531524
],
532525
any_order=True,
533526
)
534-
assert mock_heater_on.call_count == 0
535-
assert mock_heater_off.call_count >= 1
527+
assert mock_set_hvac_mode.call_count >= 1
536528
assert entity.last_temperature_slope == -1
537529
assert entity._window_auto_algo.is_window_open_detected() is True
538530
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
543535
with patch(
544536
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
545537
) as mock_send_event, patch(
546-
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
547-
) as mock_heater_on, patch(
548-
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
549-
) as mock_heater_off, patch(
538+
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
539+
) as mock_set_hvac_mode, patch(
550540
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
551541
return_value=False,
552542
):
553543
await asyncio.sleep(0.3)
554544

555-
assert mock_heater_on.call_count == 1
556-
assert mock_heater_off.call_count == 0
545+
assert mock_set_hvac_mode.call_count == 1
557546
assert round(entity.last_temperature_slope, 3) == -1
558547
# Because the algorithm is not aware of the expiration, for the algo we are still in alert
559548
assert entity._window_auto_algo.is_window_open_detected() is True
@@ -674,12 +663,11 @@ async def test_window_auto_no_on_percent(
674663
# Clean the entity
675664
entity.remove_thermostat()
676665

677-
#PR - Adding Window Bypass
666+
667+
# PR - Adding Window Bypass
678668
@pytest.mark.parametrize("expected_lingering_tasks", [True])
679669
@pytest.mark.parametrize("expected_lingering_timers", [True])
680-
async def test_window_bypass(
681-
hass: HomeAssistant, skip_hass_states_is_state
682-
):
670+
async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
683671
"""Test the Window management when bypass enabled"""
684672

685673
entry = MockConfigEntry(
@@ -810,7 +798,8 @@ async def test_window_bypass(
810798
# Clean the entity
811799
entity.remove_thermostat()
812800

813-
#PR - Adding Window bypass for window auto algorithm
801+
802+
# PR - Adding Window bypass for window auto algorithm
814803
@pytest.mark.parametrize("expected_lingering_tasks", [True])
815804
@pytest.mark.parametrize("expected_lingering_timers", [True])
816805
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
921910
# Clean the entity
922911
entity.remove_thermostat()
923912

924-
#PR - Adding Window bypass AFTER detection have been done should reactivate the heater
913+
914+
# PR - Adding Window bypass AFTER detection have been done should reactivate the heater
925915
@pytest.mark.parametrize("expected_lingering_tasks", [True])
926916
@pytest.mark.parametrize("expected_lingering_timers", [True])
927917
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
10491039
)
10501040

10511041
# Clean the entity
1052-
entity.remove_thermostat()
1042+
entity.remove_thermostat()

0 commit comments

Comments
 (0)