From ee42a235c02b56e9a28fc10add0a594c00c5eb06 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sat, 21 Dec 2024 17:24:24 +0100 Subject: [PATCH] Issue #698 nb device active for boiler not updating (#728) * With tests and list of active * Issue #698 - nb nb_device_active_for_boiler don't work for climate without hvac_action --------- Co-authored-by: Jean-Marc Collin --- .../versatile_thermostat/base_thermostat.py | 16 +- .../versatile_thermostat/sensor.py | 38 +- .../thermostat_climate_valve.py | 9 +- tests/commons.py | 1 + tests/test_central_boiler.py | 394 +++++++++++++++++- tests/test_overclimate_valve.py | 10 +- 6 files changed, 421 insertions(+), 47 deletions(-) diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 1578f28..d92340c 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -131,7 +131,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): "max_power_sensor_entity_id", "temperature_unit", "is_device_active", - "nb_device_actives", + "device_actives", "target_temperature_step", "is_used_by_central_boiler", "temperature_slope", @@ -1001,14 +1001,19 @@ def is_device_active(self) -> bool: return False @property - def nb_device_actives(self) -> int: - """Calculate the number of active devices""" - ret = 0 + def device_actives(self) -> int: + """Calculate the active devices""" + ret = [] for under in self._underlyings: if under.is_device_active: - ret += 1 + ret.append(under.entity_id) return ret + @property + def nb_device_actives(self) -> int: + """Calculate the number of active devices""" + return len(self.device_actives) + @property def current_temperature(self) -> float | None: """Return the sensor temperature.""" @@ -2680,6 +2685,7 @@ def update_custom_attributes(self): "timezone": str(self._current_tz), "temperature_unit": self.temperature_unit, "is_device_active": self.is_device_active, + "device_actives": self.device_actives, "nb_device_actives": self.nb_device_actives, "ema_temp": self._ema_temp, "is_used_by_central_boiler": self.is_used_by_central_boiler, diff --git a/custom_components/versatile_thermostat/sensor.py b/custom_components/versatile_thermostat/sensor.py index 62dac45..23d624b 100644 --- a/custom_components/versatile_thermostat/sensor.py +++ b/custom_components/versatile_thermostat/sensor.py @@ -644,6 +644,10 @@ class NbActiveDeviceForBoilerSensor(SensorEntity): """Representation of the threshold of the number of VTherm which should be active to activate the boiler""" + _entity_component_unrecorded_attributes = SensorEntity._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access + frozenset({"active_device_ids"}) + ) + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: """Initialize the energy sensor""" self._hass = hass @@ -653,13 +657,13 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: self._attr_unique_id = "nb_device_active_boiler" self._attr_value = self._attr_native_value = None # default value self._entities = [] - self._attr_active_device_names = [] # Holds the names of active devices + self._attr_active_device_ids = [] # Holds the entity ids of active devices @property def extra_state_attributes(self) -> dict: """Return additional attributes for the sensor.""" return { - "active_device_names": self._attr_active_device_names, + "active_device_ids": self._attr_active_device_ids, } @property @@ -765,6 +769,8 @@ async def calculate_nb_active_devices(self, event: Event): old_state is not None and new_state.state == old_state.state and new_hvac_action == old_hvac_action + # issue 698 - force recalculation when underlying climate doesn't have any hvac_action + and new_hvac_action is not None ): # A false state change return @@ -782,30 +788,28 @@ async def calculate_nb_active_devices(self, event: Event): ) nb_active = 0 - active_device_names = [] + active_device_ids = [] for entity in self._entities: - nb_active += entity.nb_device_actives + device_actives = entity.device_actives _LOGGER.debug( - "After examining the hvac_action of %s, nb_active is %s", + "After examining the hvac_action of %s, device_actives is %s", entity.name, - nb_active, + device_actives, ) - - if ( - entity.hvac_mode in [HVACMode.HEAT, HVACMode.AUTO] - and entity.hvac_action == HVACAction.HEATING - ): - for under in entity.underlying_entities: - if under.is_device_active: - nb_active += 1 - active_device_names.append(under.entity_id) + + nb_active += len(device_actives) + active_device_ids.extend(device_actives) self._attr_native_value = nb_active - self._attr_active_device_names = active_device_names + self._attr_active_device_ids = active_device_ids self.async_write_ha_state() + @property + def active_device_ids(self) -> list: + """Get the list of active device id""" + return self._attr_active_device_ids + def __str__(self): return f"VersatileThermostat-{self.name}" - diff --git a/custom_components/versatile_thermostat/thermostat_climate_valve.py b/custom_components/versatile_thermostat/thermostat_climate_valve.py index ad0b0ca..ab285d7 100644 --- a/custom_components/versatile_thermostat/thermostat_climate_valve.py +++ b/custom_components/versatile_thermostat/thermostat_climate_valve.py @@ -277,12 +277,15 @@ def is_device_active(self) -> bool: return self.valve_open_percent > 0 @property - def nb_device_actives(self) -> int: + def device_actives(self) -> int: """Calculate the number of active devices""" if self.is_device_active: - return len(self._underlyings_valve_regulation) + return [ + under.opening_degree_entity_id + for under in self._underlyings_valve_regulation + ] else: - return 0 + return [] @property def activable_underlying_entities(self) -> list | None: diff --git a/tests/commons.py b/tests/commons.py index be2986e..7477036 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -579,6 +579,7 @@ def name(self) -> str: def set_native_value(self, value: float): """Change the value""" self._attr_native_value = value + self.async_write_ha_state() async def create_thermostat( diff --git a/tests/test_central_boiler.py b/tests/test_central_boiler.py index 40c24c1..4f66432 100644 --- a/tests/test_central_boiler.py +++ b/tests/test_central_boiler.py @@ -2,7 +2,7 @@ """ Test the central_configuration """ import asyncio -from datetime import datetime +from datetime import datetime, timedelta from unittest.mock import patch, call @@ -29,6 +29,8 @@ CentralBoilerBinarySensor, ) +from custom_components.versatile_thermostat.sensor import NbActiveDeviceForBoilerSensor + from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .const import * # pylint: disable=wildcard-import, unused-wildcard-import @@ -103,7 +105,7 @@ async def test_update_central_boiler_state_simple( CONF_USE_MOTION_FEATURE: False, CONF_USE_POWER_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False, - CONF_HEATER: switch1.entity_id, + CONF_UNDERLYING_LIST: [switch1.entity_id], CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_INVERSE_SWITCH: False, CONF_TPI_COEF_INT: 0.3, @@ -147,6 +149,13 @@ async def test_update_central_boiler_state_simple( assert boiler_binary_sensor is not None assert boiler_binary_sensor.state == STATE_OFF + nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity( + hass, "sensor.nb_device_active_for_boiler", "sensor" + ) + assert nb_device_active_sensor is not None + assert nb_device_active_sensor.state == 0 + assert nb_device_active_sensor.active_device_ids == [] + # 1. start a heater with patch( "homeassistant.core.ServiceRegistry.async_call" @@ -195,6 +204,9 @@ async def test_update_central_boiler_state_simple( assert api.nb_active_device_for_boiler == 1 assert boiler_binary_sensor.state == STATE_ON + assert nb_device_active_sensor.state == 1 + assert nb_device_active_sensor.active_device_ids == ["switch.switch1"] + # 2. stop a heater with patch( "homeassistant.core.ServiceRegistry.async_call" @@ -235,6 +247,9 @@ async def test_update_central_boiler_state_simple( assert api.nb_active_device_for_boiler == 0 assert boiler_binary_sensor.state == STATE_OFF + assert nb_device_active_sensor.state == 0 + assert nb_device_active_sensor.active_device_ids == [] + entity.remove_thermostat() @@ -272,10 +287,12 @@ async def test_update_central_boiler_state_multiple( CONF_USE_MOTION_FEATURE: False, CONF_USE_POWER_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False, - CONF_HEATER: switch1.entity_id, - CONF_HEATER_2: switch2.entity_id, - CONF_HEATER_3: switch3.entity_id, - CONF_HEATER_4: switch4.entity_id, + CONF_UNDERLYING_LIST: [ + switch1.entity_id, + switch2.entity_id, + switch3.entity_id, + switch4.entity_id, + ], CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_INVERSE_SWITCH: False, CONF_TPI_COEF_INT: 0.3, @@ -302,10 +319,18 @@ async def test_update_central_boiler_state_multiple( assert entity.underlying_entities[1].entity_id == "switch.switch2" assert entity.underlying_entities[2].entity_id == "switch.switch3" assert entity.underlying_entities[3].entity_id == "switch.switch4" - assert entity.nb_device_actives == 0 + assert entity.device_actives == [] assert api.nb_active_device_for_boiler_threshold == 1 assert api.nb_active_device_for_boiler == 0 + + nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity( + hass, "sensor.nb_device_active_for_boiler", "sensor" + ) + assert nb_device_active_sensor is not None + assert nb_device_active_sensor.state == 0 + assert nb_device_active_sensor.active_device_ids == [] + # Force the VTherm to heat await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_preset_mode(PRESET_BOOST) @@ -338,7 +363,7 @@ async def test_update_central_boiler_state_multiple( await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.HEATING - assert entity.nb_device_actives == 1 + assert entity.device_actives == ["switch.switch1"] assert mock_service_call.call_count == 1 # No switch of the boiler @@ -356,6 +381,9 @@ async def test_update_central_boiler_state_multiple( assert api.nb_active_device_for_boiler == 1 assert boiler_binary_sensor.state == STATE_OFF + assert nb_device_active_sensor.state == 1 + assert nb_device_active_sensor.active_device_ids == ["switch.switch1"] + # 2. start a 2nd heater with patch( "homeassistant.core.ServiceRegistry.async_call" @@ -368,7 +396,7 @@ async def test_update_central_boiler_state_multiple( await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.HEATING - assert entity.nb_device_actives == 2 + assert entity.device_actives == ["switch.switch1", "switch.switch2"] # Only the first heater is started by the algo assert mock_service_call.call_count == 1 @@ -388,6 +416,12 @@ async def test_update_central_boiler_state_multiple( assert api.nb_active_device_for_boiler == 2 assert boiler_binary_sensor.state == STATE_OFF + assert nb_device_active_sensor.state == 2 + assert nb_device_active_sensor.active_device_ids == [ + "switch.switch1", + "switch.switch2", + ] + # 3. start a 3rd heater with patch( "homeassistant.core.ServiceRegistry.async_call" @@ -436,6 +470,13 @@ async def test_update_central_boiler_state_multiple( assert api.nb_active_device_for_boiler == 3 assert boiler_binary_sensor.state == STATE_ON + assert nb_device_active_sensor.state == 3 + assert nb_device_active_sensor.active_device_ids == [ + "switch.switch1", + "switch.switch2", + "switch.switch3", + ] + # 4. start a 4th heater with patch( "homeassistant.core.ServiceRegistry.async_call" @@ -466,6 +507,14 @@ async def test_update_central_boiler_state_multiple( assert api.nb_active_device_for_boiler == 4 assert boiler_binary_sensor.state == STATE_ON + assert nb_device_active_sensor.state == 4 + assert nb_device_active_sensor.active_device_ids == [ + "switch.switch1", + "switch.switch2", + "switch.switch3", + "switch.switch4", + ] + # 5. stop a heater with patch( "homeassistant.core.ServiceRegistry.async_call" @@ -484,6 +533,13 @@ async def test_update_central_boiler_state_multiple( assert api.nb_active_device_for_boiler == 3 assert boiler_binary_sensor.state == STATE_ON + assert nb_device_active_sensor.state == 3 + assert nb_device_active_sensor.active_device_ids == [ + "switch.switch2", + "switch.switch3", + "switch.switch4", + ] + # 6. stop a 2nd heater with patch( "homeassistant.core.ServiceRegistry.async_call" @@ -524,6 +580,12 @@ async def test_update_central_boiler_state_multiple( assert api.nb_active_device_for_boiler == 2 assert boiler_binary_sensor.state == STATE_OFF + assert nb_device_active_sensor.state == 2 + assert nb_device_active_sensor.active_device_ids == [ + "switch.switch2", + "switch.switch3", + ] + entity.remove_thermostat() @@ -558,7 +620,7 @@ async def test_update_central_boiler_state_simple_valve( CONF_USE_MOTION_FEATURE: False, CONF_USE_POWER_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False, - CONF_VALVE: valve1.entity_id, + CONF_UNDERLYING_LIST: [valve1.entity_id], CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_INVERSE_SWITCH: False, CONF_TPI_COEF_INT: 0.3, @@ -594,7 +656,7 @@ async def test_update_central_boiler_state_simple_valve( now: datetime = datetime.now(tz=tz) assert entity.hvac_mode == HVACMode.HEAT - assert entity.nb_device_actives == 0 + assert entity.device_actives == [] boiler_binary_sensor: CentralBoilerBinarySensor = search_entity( hass, "binary_sensor.central_boiler", "binary_sensor" @@ -602,6 +664,13 @@ async def test_update_central_boiler_state_simple_valve( assert boiler_binary_sensor is not None assert boiler_binary_sensor.state == STATE_OFF + nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity( + hass, "sensor.nb_device_active_for_boiler", "sensor" + ) + assert nb_device_active_sensor is not None + assert nb_device_active_sensor.state == 0 + assert nb_device_active_sensor.active_device_ids == [] + # 1. start a valve with patch( "homeassistant.core.ServiceRegistry.async_call" @@ -616,7 +685,7 @@ async def test_update_central_boiler_state_simple_valve( await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.HEATING - assert entity.nb_device_actives == 1 + assert entity.device_actives == ["number.valve1"] assert mock_service_call.call_count >= 1 mock_service_call.assert_has_calls( @@ -644,6 +713,11 @@ async def test_update_central_boiler_state_simple_valve( assert api.nb_active_device_for_boiler == 1 assert boiler_binary_sensor.state == STATE_ON + assert nb_device_active_sensor.state == 1 + assert nb_device_active_sensor.active_device_ids == [ + "number.valve1", + ] + # 2. stop a heater with patch( "homeassistant.core.ServiceRegistry.async_call" @@ -658,7 +732,7 @@ async def test_update_central_boiler_state_simple_valve( await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.IDLE - assert entity.nb_device_actives == 0 + assert entity.device_actives == [] assert mock_service_call.call_count >= 1 mock_service_call.assert_has_calls( @@ -687,6 +761,9 @@ async def test_update_central_boiler_state_simple_valve( assert api.nb_active_device_for_boiler == 0 assert boiler_binary_sensor.state == STATE_OFF + assert nb_device_active_sensor.state == 0 + assert nb_device_active_sensor.active_device_ids == [] + entity.remove_thermostat() @@ -721,7 +798,7 @@ async def test_update_central_boiler_state_simple_climate( CONF_USE_MOTION_FEATURE: False, CONF_USE_POWER_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False, - CONF_CLIMATE: climate1.entity_id, + CONF_UNDERLYING_LIST: [climate1.entity_id], CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_MIN_ON_PERCENT: 0.3, @@ -748,6 +825,13 @@ async def test_update_central_boiler_state_simple_climate( assert api.nb_active_device_for_boiler_threshold == 1 assert api.nb_active_device_for_boiler == 0 + nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity( + hass, "sensor.nb_device_active_for_boiler", "sensor" + ) + assert nb_device_active_sensor is not None + assert nb_device_active_sensor.state == 0 + assert nb_device_active_sensor.active_device_ids == [] + # Force the VTherm to heat await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_preset_mode(PRESET_BOOST) @@ -756,7 +840,7 @@ async def test_update_central_boiler_state_simple_climate( now: datetime = datetime.now(tz=tz) assert entity.hvac_mode == HVACMode.HEAT - assert entity.nb_device_actives == 0 + assert entity.device_actives == [] boiler_binary_sensor: CentralBoilerBinarySensor = search_entity( hass, "binary_sensor.central_boiler", "binary_sensor" @@ -779,7 +863,7 @@ async def test_update_central_boiler_state_simple_climate( await asyncio.sleep(0.5) assert entity.hvac_action == HVACAction.HEATING - assert entity.nb_device_actives == 1 + assert entity.device_actives == ["climate.climate1"] assert mock_service_call.call_count >= 1 mock_service_call.assert_has_calls( @@ -807,6 +891,11 @@ async def test_update_central_boiler_state_simple_climate( assert api.nb_active_device_for_boiler == 1 assert boiler_binary_sensor.state == STATE_ON + assert nb_device_active_sensor.state == 1 + assert nb_device_active_sensor.active_device_ids == [ + "climate.climate1", + ] + # 2. stop a climate with patch( "homeassistant.core.ServiceRegistry.async_call" @@ -821,7 +910,7 @@ async def test_update_central_boiler_state_simple_climate( await asyncio.sleep(0.5) assert entity.hvac_action == HVACAction.IDLE - assert entity.nb_device_actives == 0 + assert entity.device_actives == [] assert mock_service_call.call_count >= 1 mock_service_call.assert_has_calls( @@ -850,6 +939,277 @@ async def test_update_central_boiler_state_simple_climate( assert api.nb_active_device_for_boiler == 0 assert boiler_binary_sensor.state == STATE_OFF + assert nb_device_active_sensor.state == 0 + assert nb_device_active_sensor.active_device_ids == [] + + entity.remove_thermostat() + + +async def test_update_central_boiler_state_simple_climate_valve_regulation( + hass: HomeAssistant, + # skip_hass_states_is_state, + # skip_hass_states_get, + init_central_config_with_boiler_fixture, +): + """Test that the central boiler state behavior with a climate with valve regulation""" + + api = VersatileThermostatAPI.get_vtherm_api(hass) + + climate1 = MockClimate(hass, "climate1", "theClimate1") + + entry = MockConfigEntry( + domain=DOMAIN, + title="TheOverClimateMockName", + unique_id="uniqueId", + data={ + 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, + 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_UNDERLYING_LIST: [climate1.entity_id], + CONF_OPENING_DEGREE_LIST: ["number.mock_opening_degree"], + CONF_CLOSING_DEGREE_LIST: [], + CONF_OFFSET_CALIBRATION_LIST: [], + CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE, + CONF_AUTO_REGULATION_DTEMP: 0, + CONF_AUTO_REGULATION_PERIOD_MIN: 0, + CONF_TPI_COEF_INT: 0.3, + CONF_TPI_COEF_EXT: 0.1, + CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, + CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH, + CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False, + 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_USE_MAIN_CENTRAL_CONFIG: True, + CONF_USE_PRESETS_CENTRAL_CONFIG: True, + CONF_USE_ADVANCED_CENTRAL_CONFIG: True, + CONF_USED_BY_CENTRAL_BOILER: True, + }, + ) + + open_degree_entity = MockNumber(hass, "mock_opening_degree", "Opening degree") + open_degree_entity.set_native_value(0) + + # mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list + mock_get_state_side_effect = SideEffects( + { + open_degree_entity.entity_id: State( + open_degree_entity.entity_id, + open_degree_entity.state, + {"min": 0, "max": 100}, + ), + "number.mock_closing_degree": State( + "number.mock_closing_degree", "0", {"min": 0, "max": 100} + ), + "number.mock_offset_calibration": State( + "number.mock_offset_calibration", "0", {"min": -12, "max": 12} + ), + }, + State("unknown.entity_id", "unknown"), + ) + + with patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", + return_value=climate1, + ), patch( + "homeassistant.core.StateMachine.get", + side_effect=mock_get_state_side_effect.get_side_effects(), + ): + entity: ThermostatOverClimate = await create_thermostat( + hass, entry, "climate.theoverclimatemockname" + ) + assert entity + assert entity.name == "TheOverClimateMockName" + assert entity.is_over_climate + assert entity.underlying_entities[0].entity_id == "climate.climate1" + + assert api.nb_active_device_for_boiler_threshold == 1 + assert api.nb_active_device_for_boiler == 0 + + nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity( + hass, "sensor.nb_device_active_for_boiler", "sensor" + ) + assert nb_device_active_sensor is not None + assert nb_device_active_sensor.state == 0 + assert nb_device_active_sensor.active_device_ids == [] + + # Force the VTherm to heat + tz = get_tz(hass) # pylint: disable=invalid-name + now: datetime = datetime.now(tz=tz) + entity._set_now(now) + + await send_temperature_change_event(entity, 30, now) + await send_ext_temperature_change_event(entity, 30, now) + await hass.async_block_till_done() + + await entity.async_set_hvac_mode(HVACMode.HEAT) + await entity.async_set_preset_mode(PRESET_BOOST) + + # the VTherm should not heat now + assert entity.hvac_mode == HVACMode.HEAT + assert entity.hvac_action == HVACAction.OFF + assert entity.activable_underlying_entities[0]._percent_open == 0 + assert entity.device_actives == [] + + boiler_binary_sensor: CentralBoilerBinarySensor = search_entity( + hass, "binary_sensor.central_boiler", "binary_sensor" + ) + assert boiler_binary_sensor is not None + assert boiler_binary_sensor.state == STATE_OFF + + # 1. start a climate + open_degree_entity.set_native_value(100) + mock_get_state_side_effect = SideEffects( + { + open_degree_entity.entity_id: State( + open_degree_entity.entity_id, + open_degree_entity.state, + {"min": 0, "max": 100}, + ), + "number.mock_closing_degree": State( + "number.mock_closing_degree", "0", {"min": 0, "max": 100} + ), + "number.mock_offset_calibration": State( + "number.mock_offset_calibration", "0", {"min": -12, "max": 12} + ), + }, + State("unknown.entity_id", "unknown"), + ) + + with patch( + "homeassistant.core.ServiceRegistry.async_call" + ) as mock_service_call, patch( + "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" + ) as mock_send_event, patch( + "homeassistant.core.StateMachine.get", + side_effect=mock_get_state_side_effect.get_side_effects(), + ): + now = now + timedelta(minutes=1) + entity._set_now(now) + + await send_temperature_change_event(entity, 10, now) + # we have to simulate the climate also else the test don't work + climate1.set_hvac_mode(HVACMode.HEAT) + climate1.set_hvac_action(HVACAction.HEATING) + climate1.async_write_ha_state() + open_degree_entity.set_native_value(100) + # Wait for state event propagation + await hass.async_block_till_done() + + assert entity.hvac_action == HVACAction.HEATING + assert entity.device_actives == ["number.mock_opening_degree"] + + assert api.nb_active_device_for_boiler == 1 + assert boiler_binary_sensor.state == STATE_ON + + assert nb_device_active_sensor.state == 1 + assert nb_device_active_sensor.active_device_ids == [ + "number.mock_opening_degree", + ] + + assert mock_service_call.call_count >= 1 + mock_service_call.assert_has_calls( + [ + call.service_call( + "switch", + "turn_on", + service_data={}, + target={"entity_id": "switch.pompe_chaudiere"}, + ), + ] + ) + assert mock_send_event.call_count >= 1 + mock_send_event.assert_has_calls( + [ + call.send_vtherm_event( + hass=hass, + event_type=EventType.CENTRAL_BOILER_EVENT, + entity=api.central_boiler_entity, + data={"central_boiler": True}, + ) + ] + ) + + # 2. stop a climate + open_degree_entity.set_native_value(0) + mock_get_state_side_effect = SideEffects( + { + open_degree_entity.entity_id: State( + open_degree_entity.entity_id, + open_degree_entity.state, + {"min": 0, "max": 100}, + ), + "number.mock_closing_degree": State( + "number.mock_closing_degree", "0", {"min": 0, "max": 100} + ), + "number.mock_offset_calibration": State( + "number.mock_offset_calibration", "0", {"min": -12, "max": 12} + ), + }, + State("unknown.entity_id", "unknown"), + ) + with patch( + "homeassistant.core.ServiceRegistry.async_call" + ) as mock_service_call, patch( + "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" + ) as mock_send_event, patch( + "homeassistant.core.StateMachine.get", + side_effect=mock_get_state_side_effect.get_side_effects(), + ): + await send_temperature_change_event(entity, 25, now) + climate1.set_hvac_mode(HVACMode.HEAT) + climate1.set_hvac_action(HVACAction.IDLE) + climate1.async_write_ha_state() + open_degree_entity.set_native_value(0) + # Wait for state event propagation + await asyncio.sleep(0.5) + + assert entity.hvac_action == HVACAction.OFF + assert entity.device_actives == [] + + assert mock_service_call.call_count >= 1 + mock_service_call.assert_has_calls( + [ + call( + "switch", + "turn_off", + service_data={}, + target={"entity_id": "switch.pompe_chaudiere"}, + ) + ] + ) + + assert mock_send_event.call_count >= 1 + mock_send_event.assert_has_calls( + [ + call.send_vtherm_event( + hass=hass, + event_type=EventType.CENTRAL_BOILER_EVENT, + entity=api.central_boiler_entity, + data={"central_boiler": False}, + ) + ] + ) + + assert api.nb_active_device_for_boiler == 0 + assert boiler_binary_sensor.state == STATE_OFF + + assert nb_device_active_sensor.state == 0 + assert nb_device_active_sensor.active_device_ids == [] + entity.remove_thermostat() diff --git a/tests/test_overclimate_valve.py b/tests/test_overclimate_valve.py index d27ef75..1040891 100644 --- a/tests/test_overclimate_valve.py +++ b/tests/test_overclimate_valve.py @@ -18,10 +18,10 @@ logging.getLogger().setLevel(logging.DEBUG) -# @pytest.mark.parametrize("expected_lingering_tasks", [True]) -# @pytest.mark.parametrize("expected_lingering_timers", [True]) +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize("expected_lingering_timers", [True]) # this test fails if run in // with the next because the underlying_valve_regulation is mixed. Don't know why -@pytest.mark.skip +# @pytest.mark.skip async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get): """Test the normal full start of a thermostat in thermostat_over_climate type""" @@ -138,13 +138,13 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get 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'}), call("climate","set_temperature",{ "entity_id": "climate.mock_climate", "temperature": 15, # temp-min }, ), - 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'}), # we have no current_temperature yet # call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}), ]