# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long """ Test the central_configuration """ import asyncio from datetime import datetime, timedelta from unittest.mock import patch, call from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntryState from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.versatile_thermostat.thermostat_switch import ( ThermostatOverSwitch, ) from custom_components.versatile_thermostat.thermostat_valve import ( ThermostatOverValve, ) from custom_components.versatile_thermostat.thermostat_climate import ( ThermostatOverClimate, ) from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI from custom_components.versatile_thermostat.binary_sensor import ( 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 # @pytest.mark.parametrize("expected_lingering_tasks", [True]) # @pytest.mark.parametrize("expected_lingering_timers", [True]) async def test_add_a_central_config_with_boiler( hass: HomeAssistant, skip_hass_states_is_state, ): """Tests the clean_central_config_doubon of base_thermostat""" central_config_entry = MockConfigEntry( domain=DOMAIN, title="TheCentralConfigMockName", unique_id="centralConfigUniqueId", data=FULL_CENTRAL_CONFIG_WITH_BOILER, ) central_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(central_config_entry.entry_id) assert central_config_entry.state is ConfigEntryState.LOADED entity: ThermostatOverClimate = search_entity( hass, "climate.thecentralconfigmockname", "climate" ) assert entity is None assert count_entities(hass, "climate.thecentralconfigmockname", "climate") == 0 # Test that VTherm API find the CentralConfig api = VersatileThermostatAPI.get_vtherm_api(hass) central_configuration = api.find_central_configuration() assert central_configuration is not None # Test that VTherm API have any central boiler entities assert api.nb_active_device_for_boiler_entity is not None assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler_threshold_entity is not None assert api.nb_active_device_for_boiler_threshold == 1 # the default value is 1 async def test_update_central_boiler_state_simple( hass: HomeAssistant, # skip_hass_states_is_state, init_central_config_with_boiler_fixture, ): """Test that the central boiler state behavoir""" api = VersatileThermostatAPI.get_vtherm_api(hass) switch1 = MockSwitch(hass, "switch1", "theSwitch1") entry = MockConfigEntry( domain=DOMAIN, title="TheOverSwitchMockName", unique_id="uniqueId", data={ CONF_NAME: "TheOverSwitchMockName", CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, 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: [switch1.entity_id], CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_INVERSE_SWITCH: False, CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_EXT: 0.01, CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_MIN_ON_PERCENT: 0.3, CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1, CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_TPI_CENTRAL_CONFIG: True, CONF_USE_PRESETS_CENTRAL_CONFIG: True, CONF_USE_ADVANCED_CENTRAL_CONFIG: True, CONF_USED_BY_CENTRAL_BOILER: True, }, ) entity: ThermostatOverSwitch = await create_thermostat( hass, entry, "climate.theoverswitchmockname" ) assert entity assert entity.name == "TheOverSwitchMockName" assert entity.is_over_switch assert entity.underlying_entities[0].entity_id == "switch.switch1" assert api.nb_active_device_for_boiler_threshold == 1 assert api.nb_active_device_for_boiler == 0 # Force the VTherm to heat await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_preset_mode(PRESET_BOOST) tz = get_tz(hass) # pylint: disable=invalid-name now: datetime = datetime.now(tz=tz) await send_temperature_change_event(entity, 10, now) assert entity.hvac_mode == HVACMode.HEAT 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 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" ) as mock_service_call, patch( "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" ) as mock_send_event: await switch1.async_turn_on() switch1.async_write_ha_state() # Wait for state event propagation await asyncio.sleep(1) assert entity.hvac_action == HVACAction.HEATING assert mock_service_call.call_count == 2 # Sometimes this test fails mock_service_call.assert_has_calls( [ call.service_call( "switch", "turn_on", {"entity_id": "switch.switch1"}, ), call( "switch", "turn_on", service_data={}, target={"entity_id": "switch.pompe_chaudiere"}, ), ], any_order=True, ) 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}, ) ] ) 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" ) as mock_service_call, patch( "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" ) as mock_send_event: await switch1.async_turn_off() switch1.async_write_ha_state() # Wait for state event propagation await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.IDLE 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() async def test_update_central_boiler_state_multiple( hass: HomeAssistant, # skip_hass_states_is_state, init_central_config_with_boiler_fixture, ): """Test that the central boiler state behavoir""" api = VersatileThermostatAPI.get_vtherm_api(hass) switch1 = MockSwitch(hass, "switch1", "theSwitch1") switch2 = MockSwitch(hass, "switch2", "theSwitch2") switch3 = MockSwitch(hass, "switch3", "theSwitch3") switch4 = MockSwitch(hass, "switch4", "theSwitch4") entry = MockConfigEntry( domain=DOMAIN, title="TheOverSwitchMockName", unique_id="uniqueId", data={ CONF_NAME: "TheOverSwitchMockName", CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, 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: [ 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, CONF_TPI_COEF_EXT: 0.01, CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_MIN_ON_PERCENT: 0.3, CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1, CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_TPI_CENTRAL_CONFIG: True, CONF_USE_PRESETS_CENTRAL_CONFIG: True, CONF_USE_ADVANCED_CENTRAL_CONFIG: True, CONF_USED_BY_CENTRAL_BOILER: True, }, ) entity: ThermostatOverSwitch = await create_thermostat( hass, entry, "climate.theoverswitchmockname" ) assert entity assert entity.name == "TheOverSwitchMockName" assert entity.is_over_switch assert entity.underlying_entities[0].entity_id == "switch.switch1" 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.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) tz = get_tz(hass) # pylint: disable=invalid-name now: datetime = datetime.now(tz=tz) await send_temperature_change_event(entity, 10, now) assert entity.hvac_mode == HVACMode.HEAT # 0. set threshold to 3 api.nb_active_device_for_boiler_threshold_entity.set_native_value(3) assert api.nb_active_device_for_boiler_threshold == 3 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 first heater 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: await switch1.async_turn_on() switch1.async_write_ha_state() # Wait for state event propagation await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.HEATING assert entity.device_actives == ["switch.switch1"] assert mock_service_call.call_count == 1 # No switch of the boiler mock_service_call.assert_has_calls( [ call.service_call( "switch", "turn_on", {"entity_id": "switch.switch1"}, ), ] ) assert mock_send_event.call_count == 0 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" ) as mock_service_call, patch( "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" ) as mock_send_event: await switch2.async_turn_on() switch2.async_write_ha_state() # Wait for state event propagation await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.HEATING assert entity.device_actives == ["switch.switch1", "switch.switch2"] # Only the first heater is started by the algo assert mock_service_call.call_count == 1 # No switch of the boiler. Caution: each time a underlying heater state change itself, # the cycle restarts. So it is always the first heater that is started mock_service_call.assert_has_calls( [ call.service_call( "switch", "turn_on", {"entity_id": "switch.switch1"}, ), ] ) assert mock_send_event.call_count == 0 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" ) as mock_service_call, patch( "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" ) as mock_send_event: await switch3.async_turn_on() switch3.async_write_ha_state() # Wait for state event propagation await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.HEATING # Only the first heater is started by the algo assert mock_service_call.call_count == 2 # No switch of the boiler. Caution: each time a underlying heater state change itself, # the cycle restarts. So it is always the first heater that is started mock_service_call.assert_has_calls( [ call.service_call( "switch", "turn_on", service_data={}, target={"entity_id": "switch.pompe_chaudiere"}, ), call.service_call( "switch", "turn_on", {"entity_id": "switch.switch1"}, ), ], any_order=True, ) 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}, ) ] ) 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" ) as mock_service_call, patch( "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" ) as mock_send_event: await switch4.async_turn_on() switch4.async_write_ha_state() # Wait for state event propagation await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.HEATING # Only the first heater is started by the algo assert mock_service_call.call_count == 1 # No switch of the boiler. Caution: each time a underlying heater state change itself, # the cycle restarts. So it is always the first heater that is started mock_service_call.assert_has_calls( [ call.service_call( "switch", "turn_on", {"entity_id": "switch.switch1"}, ), ] ) assert mock_send_event.call_count == 0 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" ) as mock_service_call, patch( "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" ) as mock_send_event: await switch1.async_turn_off() switch1.async_write_ha_state() # Wait for state event propagation await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.HEATING assert mock_service_call.call_count == 0 assert mock_send_event.call_count == 0 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" ) as mock_service_call, patch( "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" ) as mock_send_event: await switch4.async_turn_off() switch4.async_write_ha_state() # Wait for state event propagation await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.HEATING 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 == 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() async def test_update_central_boiler_state_simple_valve( hass: HomeAssistant, # skip_hass_states_is_state, init_central_config_with_boiler_fixture, ): """Test that the central boiler state behavoir""" api = VersatileThermostatAPI.get_vtherm_api(hass) valve1 = MockNumber(hass, "valve1", "theValve1") entry = MockConfigEntry( domain=DOMAIN, title="TheOverValveMockName", unique_id="uniqueId", data={ CONF_NAME: "TheOverValveMockName", CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE, 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: [valve1.entity_id], CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_INVERSE_SWITCH: False, CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_EXT: 0.01, CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_MIN_ON_PERCENT: 0.3, CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1, CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_TPI_CENTRAL_CONFIG: True, CONF_USE_PRESETS_CENTRAL_CONFIG: True, CONF_USE_ADVANCED_CENTRAL_CONFIG: True, CONF_USED_BY_CENTRAL_BOILER: True, }, ) entity: ThermostatOverValve = await create_thermostat( hass, entry, "climate.theovervalvemockname" ) assert entity assert entity.name == "TheOverValveMockName" assert entity.is_over_valve assert entity.underlying_entities[0].entity_id == "number.valve1" assert api.nb_active_device_for_boiler_threshold == 1 assert api.nb_active_device_for_boiler == 0 # Force the VTherm to heat await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_preset_mode(PRESET_BOOST) tz = get_tz(hass) # pylint: disable=invalid-name now: datetime = datetime.now(tz=tz) assert entity.hvac_mode == HVACMode.HEAT 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 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" ) as mock_service_call, patch( "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" ) as mock_send_event: await send_temperature_change_event(entity, 10, now) # we have to simulate the valve also else the test don't work valve1.set_native_value(10) valve1.async_write_ha_state() # Wait for state event propagation await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.HEATING assert entity.device_actives == ["number.valve1"] 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}, ) ] ) 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" ) as mock_service_call, patch( "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" ) as mock_send_event: await send_temperature_change_event(entity, 25, now) # Change the valve value to 0 valve1.set_native_value(0) valve1.async_write_ha_state() # Wait for state event propagation await asyncio.sleep(0.1) assert entity.hvac_action == HVACAction.IDLE 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() async def test_update_central_boiler_state_simple_climate( hass: HomeAssistant, # skip_hass_states_is_state, init_central_config_with_boiler_fixture, ): """Test that the central boiler state behavoir""" 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_MINIMAL_ACTIVATION_DELAY: 30, CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_MIN_ON_PERCENT: 0.3, CONF_SAFETY_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, }, ) with patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", return_value=climate1, ): 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 await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_preset_mode(PRESET_BOOST) tz = get_tz(hass) # pylint: disable=invalid-name now: datetime = datetime.now(tz=tz) assert entity.hvac_mode == HVACMode.HEAT 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 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: 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() # Wait for state event propagation await asyncio.sleep(0.5) assert entity.hvac_action == HVACAction.HEATING assert entity.device_actives == ["climate.climate1"] 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}, ) ] ) 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" ) as mock_service_call, patch( "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" ) as mock_send_event: 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() # Wait for state event propagation await asyncio.sleep(0.5) assert entity.hvac_action == HVACAction.IDLE 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() 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_SAFETY_DELAY_MIN: 5, CONF_SAFETY_MIN_ON_PERCENT: 0.3, CONF_SAFETY_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() async def test_bug_339( hass: HomeAssistant, # skip_hass_states_is_state, init_central_config_with_boiler_fixture, ): """Test that the counter of active Vtherm in central boiler is correctly updated with underlying is in auto and device is active """ api = VersatileThermostatAPI.get_vtherm_api(hass) climate1 = MockClimate( hass=hass, unique_id="climate1", name="theClimate1", hvac_mode=HVACMode.AUTO, hvac_modes=[HVACMode.AUTO, HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL], hvac_action=HVACAction.HEATING, ) 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_CLIMATE: climate1.entity_id, CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_MIN_ON_PERCENT: 0.3, CONF_SAFETY_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, }, ) with patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", return_value=climate1, ): entity: ThermostatOverValve = 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 await entity.async_set_hvac_mode(HVACMode.AUTO) # Simulate a state change in underelying await api.nb_active_device_for_boiler_entity.calculate_nb_active_devices(None) # The VTherm should be active assert entity.underlying_entity(0).is_device_active is True assert entity.is_device_active is True assert api.nb_active_device_for_boiler == 1 entity.remove_thermostat()