Skip to content

Commit 8d90bdf

Browse files
jmcollin78Jean-Marc Collin
andauthored
#1407 - Fix Total power for active boiler does not count vtherms over climate ? (#1435)
Fixes #1407 Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
1 parent e37904c commit 8d90bdf

4 files changed

Lines changed: 157 additions & 9 deletions

File tree

custom_components/versatile_thermostat/feature_power_manager.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,13 @@ def device_power(self) -> float:
293293
@property
294294
def mean_cycle_power(self) -> float | None:
295295
"""Returns the mean power consumption during the cycle"""
296-
if not self._device_power or not self._vtherm.proportional_algorithm:
296+
if not self._device_power:
297+
return None
298+
299+
if self._vtherm.is_over_climate:
300+
return self._device_power if self._vtherm.is_device_active else 0.0
301+
302+
if not self._vtherm.proportional_algorithm:
297303
return None
298304

299305
return float(round_to_nearest(self._device_power * self._vtherm.proportional_algorithm.on_percent, 0.1))

custom_components/versatile_thermostat/sensor.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,16 +190,18 @@ async def async_my_climate_changed(self, event: Event = None):
190190
"""Called when my climate have change"""
191191
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
192192

193-
if math.isnan(
194-
float(self.my_climate.power_manager.mean_cycle_power)
195-
) or math.isinf(self.my_climate.power_manager.mean_cycle_power):
196-
raise ValueError(
197-
f"Sensor has illegal state {self.my_climate.power_manager.mean_cycle_power}"
198-
)
193+
mean_cycle_power = self.my_climate.power_manager.mean_cycle_power
194+
if mean_cycle_power is None:
195+
return
196+
197+
if math.isnan(float(mean_cycle_power)) or math.isinf(float(mean_cycle_power)):
198+
raise ValueError(f"Sensor has illegal state {mean_cycle_power}")
199+
200+
mean_cycle_power = float(mean_cycle_power)
199201

200202
old_state = self._attr_native_value
201203
self._attr_native_value = round(
202-
self.my_climate.power_manager.mean_cycle_power,
204+
mean_cycle_power,
203205
self.suggested_display_precision,
204206
)
205207
if old_state != self._attr_native_value:

tests/test_central_boiler.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,144 @@ async def test_update_central_boiler_state_simple_climate(
966966
entity.remove_thermostat()
967967

968968

969+
async def test_update_central_boiler_state_simple_climate_power(
970+
hass: HomeAssistant,
971+
# skip_hass_states_is_state,
972+
init_central_config_with_boiler_fixture,
973+
):
974+
"""Test that the central boiler state behavoir"""
975+
976+
api = VersatileThermostatAPI.get_vtherm_api(hass)
977+
978+
climate1 = MockClimate(hass, "climate1", "theClimate1")
979+
await register_mock_entity(hass, climate1, CLIMATE_DOMAIN)
980+
981+
switch_pompe_chaudiere = MockSwitch(hass, "pompe_chaudiere", "SwitchPompeChaudiere")
982+
await register_mock_entity(hass, switch_pompe_chaudiere, SWITCH_DOMAIN)
983+
assert switch_pompe_chaudiere.is_on is False
984+
985+
entry = MockConfigEntry(
986+
domain=DOMAIN,
987+
title="TheOverClimateMockName",
988+
unique_id="uniqueId",
989+
data={
990+
CONF_NAME: "TheOverClimateMockName",
991+
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
992+
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
993+
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
994+
CONF_CYCLE_MIN: 5,
995+
CONF_TEMP_MIN: 8,
996+
CONF_TEMP_MAX: 18,
997+
CONF_USE_WINDOW_FEATURE: False,
998+
CONF_USE_MOTION_FEATURE: False,
999+
CONF_USE_POWER_FEATURE: False,
1000+
CONF_USE_PRESENCE_FEATURE: False,
1001+
CONF_UNDERLYING_LIST: [climate1.entity_id],
1002+
CONF_MINIMAL_ACTIVATION_DELAY: 30,
1003+
CONF_MINIMAL_DEACTIVATION_DELAY: 0,
1004+
CONF_SAFETY_DELAY_MIN: 5,
1005+
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
1006+
CONF_SAFETY_DEFAULT_ON_PERCENT: 0.1,
1007+
CONF_USE_MAIN_CENTRAL_CONFIG: True,
1008+
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
1009+
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
1010+
CONF_USED_BY_CENTRAL_BOILER: True,
1011+
CONF_DEVICE_POWER: 1001,
1012+
},
1013+
)
1014+
1015+
with patch(
1016+
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
1017+
return_value=climate1,
1018+
):
1019+
entity: ThermostatOverClimate = await create_thermostat(hass, entry, "climate.theoverclimatemockname", temps=default_temperatures)
1020+
assert entity
1021+
assert entity.name == "TheOverClimateMockName"
1022+
assert entity.is_over_climate
1023+
assert entity.underlying_entities[0].entity_id == "climate.climate1"
1024+
1025+
api.central_boiler_manager._set_nb_active_device_threshold(0)
1026+
api.central_boiler_manager._set_total_power_active_threshold(1000)
1027+
assert api.central_boiler_manager.nb_active_device_for_boiler_threshold == 0
1028+
assert api.central_boiler_manager.nb_active_device_for_boiler == 0
1029+
assert api.central_boiler_manager.total_power_active_for_boiler_threshold == 1000
1030+
assert api.central_boiler_manager.total_power_active_for_boiler == 0
1031+
1032+
assert (nb_device_active_sensor := search_entity(hass, "sensor.nb_device_active_for_boiler", "sensor")) is not None
1033+
assert (total_power_active_sensor := search_entity(hass, "sensor.total_power_active_for_boiler", "sensor")) is not None
1034+
1035+
assert nb_device_active_sensor.state == 0
1036+
assert nb_device_active_sensor.active_device_ids == []
1037+
1038+
assert total_power_active_sensor.state == 0
1039+
assert total_power_active_sensor.active_device_ids == []
1040+
1041+
# Force the VTherm to heat
1042+
tz = get_tz(hass) # pylint: disable=invalid-name
1043+
now: datetime = datetime.now(tz=tz)
1044+
1045+
await send_temperature_change_event(entity, 25, now)
1046+
await entity.async_set_hvac_mode(VThermHvacMode_HEAT)
1047+
await entity.async_set_preset_mode(VThermPreset.FROST)
1048+
1049+
assert entity.hvac_mode == VThermHvacMode_HEAT
1050+
assert entity.device_actives == []
1051+
1052+
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(hass, "binary_sensor.central_boiler", "binary_sensor")
1053+
assert boiler_binary_sensor is not None
1054+
assert boiler_binary_sensor.state == STATE_OFF
1055+
1056+
# 1. start a climate
1057+
now = now + timedelta(minutes=10)
1058+
await send_temperature_change_event(entity, 10, now)
1059+
await entity.async_set_preset_mode(VThermPreset.COMFORT)
1060+
# Simulate that the climate is heating
1061+
climate1.set_hvac_mode(VThermHvacMode_HEAT)
1062+
climate1.set_hvac_action(HVACAction.HEATING)
1063+
climate1.async_write_ha_state()
1064+
# Wait for state event propagation
1065+
await asyncio.sleep(0.5)
1066+
1067+
assert entity.hvac_action == HVACAction.HEATING
1068+
assert entity.device_actives == ["climate.climate1"]
1069+
1070+
assert api.central_boiler_manager.nb_active_device_for_boiler == 1
1071+
assert api.central_boiler_manager.total_power_active_for_boiler == 1001
1072+
assert boiler_binary_sensor.state == STATE_ON
1073+
1074+
assert nb_device_active_sensor.state == 1
1075+
assert nb_device_active_sensor.active_device_ids == [
1076+
"climate.climate1",
1077+
]
1078+
1079+
# Power is not configured
1080+
assert total_power_active_sensor.state == 1001
1081+
assert total_power_active_sensor.active_device_ids == ["climate.climate1"]
1082+
1083+
# 2. stop a climate
1084+
await send_temperature_change_event(entity, 25, now)
1085+
climate1.set_hvac_mode(VThermHvacMode_HEAT)
1086+
climate1.set_hvac_action(HVACAction.IDLE)
1087+
climate1.async_write_ha_state()
1088+
# Wait for state event propagation
1089+
await asyncio.sleep(0.5)
1090+
1091+
assert entity.hvac_action == HVACAction.IDLE
1092+
assert entity.device_actives == []
1093+
1094+
assert api.central_boiler_manager.nb_active_device_for_boiler == 0
1095+
assert boiler_binary_sensor.state == STATE_OFF
1096+
1097+
assert nb_device_active_sensor.state == 0
1098+
assert nb_device_active_sensor.active_device_ids == []
1099+
1100+
# Power is not configured
1101+
assert total_power_active_sensor.state == 0
1102+
assert total_power_active_sensor.active_device_ids == []
1103+
1104+
entity.remove_thermostat()
1105+
1106+
9691107
# @pytest.mark.skip(reason="This test don't work when execute in // of other tests. It should be run alone")
9701108
async def test_update_central_boiler_state_simple_climate_valve_regulation(
9711109
hass: HomeAssistant,

tests/test_power.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ async def test_power_management_energy_over_climate(
731731
assert entity.current_temperature == 15
732732

733733
# Not initialised yet
734-
assert entity.power_manager.mean_cycle_power is None
734+
assert entity.power_manager.mean_cycle_power == 0.0
735735
assert entity._underlying_climate_start_hvac_action_date is None
736736

737737
# Send a climate_change event with HVACAction=HEATING
@@ -894,6 +894,8 @@ async def test_power_management_turn_off_while_shedding(hass: HomeAssistant, ski
894894
#
895895
#
896896
#
897+
#
898+
#
897899
# fmt:off
898900
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
899901
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"), \

0 commit comments

Comments
 (0)