Skip to content

Commit

Permalink
Fix too much shedding in over_climate
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-Marc Collin committed Jan 5, 2025
1 parent 4602816 commit 34181b4
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 191 deletions.
24 changes: 15 additions & 9 deletions custom_components/versatile_thermostat/base_thermostat.py
Original file line number Diff line number Diff line change
Expand Up @@ -1609,14 +1609,8 @@ async def async_control_heating(self, force=False, _=None) -> bool:
return False

# Check overpowering condition
await VersatileThermostatAPI.get_vtherm_api().central_power_manager.refresh_state()

# TODO remove this
# overpowering is now centralized
# overpowering = await self._power_manager.check_overpowering()
# if overpowering == STATE_ON:
# _LOGGER.debug("%s - End of cycle (overpowering)", self)
# return True
# Not usefull. Will be done at the next power refresh
# await VersatileThermostatAPI.get_vtherm_api().central_power_manager.refresh_state()

safety: bool = await self._safety_manager.refresh_state()
if safety and self.is_over_climate:
Expand Down Expand Up @@ -1974,5 +1968,17 @@ def now(self) -> datetime:

@property
def power_percent(self) -> float | None:
"""Get the current on_percent as a percentage value. valid only for Vtherm with a TPI algo"""
"""Get the current on_percent value"""
if self._prop_algorithm and self._prop_algorithm.on_percent is not None:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None

@property
def on_percent(self) -> float | None:
"""Get the current on_percent value. valid only for Vtherm with a TPI algo"""
return None
if self._prop_algorithm and self._prop_algorithm.on_percent is not None:
return self._prop_algorithm.on_percent
else:
return None
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any
from functools import cmp_to_key

from homeassistant.const import STATE_OFF
from homeassistant.core import HomeAssistant, Event, callback
from homeassistant.helpers.event import (
async_track_state_change_event,
Expand Down Expand Up @@ -163,54 +164,52 @@ async def calculate_shedding(self):
total_power_gain = 0

for vtherm in vtherms_sorted:
device_power = vtherm.power_manager.device_power
if vtherm.is_device_active and not vtherm.power_manager.is_overpowering_detected:
device_power = vtherm.power_manager.device_power
total_power_gain += device_power
_LOGGER.debug("vtherm %s should be in overpowering state", vtherm.name)
_LOGGER.info("vtherm %s should be in overpowering state (device_power=%.2f)", vtherm.name, device_power)
await vtherm.power_manager.set_overpowering(True, device_power)

_LOGGER.debug("after vtherm %s total_power_gain=%s, available_power=%s", vtherm.name, total_power_gain, available_power)
if total_power_gain >= -available_power:
_LOGGER.debug("We have found enough vtherm to set to overpowering")
break
# unshedding only
else:
# vtherms_sorted.reverse()
vtherms_sorted.reverse()
_LOGGER.debug("The available power is is > 0 (%s). Do a complete shedding/un-shedding calculation for list: %s", available_power, vtherms_sorted)

total_affected_power = 0
force_overpowering = False
total_power_added = 0

for vtherm in vtherms_sorted:
device_power = vtherm.power_manager.device_power
# We want to do always unshedding in order to initialize the state
# so we cannot use is_overpowering_detected which test also UNKNOWN and UNAVAILABLE
if vtherm.power_manager.overpowering_state == STATE_OFF:
continue

power_consumption_max = device_power = vtherm.power_manager.device_power
# calculate the power_consumption_max
if vtherm.is_device_active:
power_consumption_max = 0
else:
if vtherm.is_over_climate:
power_consumption_max = device_power
else:
if vtherm.proportional_algorithm.on_percent > 0:
power_consumption_max = max(
device_power / vtherm.nb_underlying_entities,
device_power * vtherm.proportional_algorithm.on_percent,
)
else:
power_consumption_max = 0
if vtherm.on_percent is not None:
power_consumption_max = max(
device_power / vtherm.nb_underlying_entities,
device_power * vtherm.on_percent,
)

_LOGGER.debug("vtherm %s power_consumption_max is %s (device_power=%s, overclimate=%s)", vtherm.name, power_consumption_max, device_power, vtherm.is_over_climate)
if force_overpowering or (total_affected_power + power_consumption_max >= available_power):
_LOGGER.debug("vtherm %s should be in overpowering state", vtherm.name)
if not vtherm.power_manager.is_overpowering_detected:
# To force all others vtherms to be in overpowering
force_overpowering = True
await vtherm.power_manager.set_overpowering(True, power_consumption_max)
else:
total_affected_power += power_consumption_max
# Always set to false to init the state
_LOGGER.debug("vtherm %s should not be in overpowering state", vtherm.name)
await vtherm.power_manager.set_overpowering(False)

_LOGGER.debug("after vtherm %s total_affected_power=%s, available_power=%s", vtherm.name, total_affected_power, available_power)
# if total_power_added + power_consumption_max < available_power or not vtherm.power_manager.is_overpowering_detected:
_LOGGER.info("vtherm %s should not be in overpowering state (power_consumption_max=%.2f)", vtherm.name, power_consumption_max)

# we count the unshedding only if the VTherm was in shedding
if vtherm.power_manager.is_overpowering_detected:
total_power_added += power_consumption_max

await vtherm.power_manager.set_overpowering(False)

if total_power_added >= available_power:
_LOGGER.debug("We have found enough vtherm to set to non-overpowering")
break

_LOGGER.debug("after vtherm %s total_power_added=%s, available_power=%s", vtherm.name, total_power_added, available_power)

def get_climate_components_entities(self) -> list:
"""Get all VTherms entitites"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,15 +263,6 @@ def have_valve_regulation(self) -> bool:
"""True if the Thermostat is regulated by valve"""
return True

@overrides
@property
def power_percent(self) -> float | None:
"""Get the current on_percent value"""
if self._prop_algorithm:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None

# @property
# def hvac_modes(self) -> list[HVACMode]:
# """Get the hvac_modes"""
Expand Down
9 changes: 0 additions & 9 deletions custom_components/versatile_thermostat/thermostat_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,6 @@ def is_inversed(self) -> bool:
"""True if the switch is inversed (for pilot wire and diode)"""
return self._is_inversed is True

@overrides
@property
def power_percent(self) -> float | None:
"""Get the current on_percent value"""
if self._prop_algorithm:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None

@overrides
def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat"""
Expand Down
11 changes: 6 additions & 5 deletions tests/test_binary_sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,16 +172,17 @@ async def test_overpowering_binary_sensors(
# Send power mesurement
side_effects = SideEffects(
{
"sensor.the_power_sensor": State("sensor.the_power_sensor", 100),
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 150),
"sensor.the_power_sensor": State("sensor.the_power_sensor", 150),
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 100),
},
State("unknown.entity_id", "unknown"),
)
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
# fmt: on
await send_power_change_event(entity, 100, now)
await send_max_power_change_event(entity, 150, now)
await send_power_change_event(entity, 150, now)
await send_max_power_change_event(entity, 100, now)

assert entity.power_manager.is_overpowering_detected is True
assert entity.power_manager.overpowering_state is STATE_ON
Expand Down
11 changes: 6 additions & 5 deletions tests/test_bugs.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ async def test_bug_407(
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 18
# waits that the heater starts
await hass.async_block_till_done()
Expand Down Expand Up @@ -398,7 +398,8 @@ async def test_bug_407(
assert entity.target_temperature == 19
assert mock_service_call.call_count >= 1

# 3. if heater is stopped (is_device_active==False), then overpowering should be started
# 3. if heater is stopped (is_device_active==False) and power is over max, then overpowering should be started
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 150))
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
Expand All @@ -420,10 +421,10 @@ async def test_bug_407(
# simulate a refresh for central power (not necessary)
await do_central_power_refresh(hass)

assert entity.power_manager.is_overpowering_detected is True
assert entity.power_manager.is_overpowering_detected is False
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_POWER
assert entity.power_manager.overpowering_state is STATE_ON
assert entity.preset_mode is PRESET_COMFORT
assert entity.power_manager.overpowering_state is STATE_OFF


@pytest.mark.parametrize("expected_lingering_tasks", [True])
Expand Down
Loading

0 comments on commit 34181b4

Please sign in to comment.