Fix too much shedding in over_climate

This commit is contained in:
Jean-Marc Collin
2025-01-05 08:44:59 +00:00
parent 460281603f
commit 34181b4204
9 changed files with 192 additions and 221 deletions

View File

@@ -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

View File

@@ -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()
@@ -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(
@@ -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])

View File

@@ -273,7 +273,7 @@ async def test_central_power_manageer_find_vtherms(
@pytest.mark.parametrize(
"current_power, current_max_power, vtherm_configs, expected_results",
[
# simple nominal test (no shedding)
# simple nominal test (initialize overpowering state in VTherm)
(
1000,
5000,
@@ -286,139 +286,32 @@ async def test_central_power_manageer_find_vtherms(
"nb_underlying_entities": 1,
"on_percent": 0,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
],
{"vtherm1": False},
),
# Simple trivial shedding
(
1000,
2000,
[
# should be overpowering
{
"name": "vtherm1",
"device_power": 1100,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
},
# should be overpowering with many underlmying entities
{
"name": "vtherm2",
"device_power": 4000,
"is_device_active": False,
"device_power": 10000,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"on_percent": 100,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
# over_climate should be overpowering
{
"name": "vtherm3",
"device_power": 1000,
"is_device_active": False,
"is_over_climate": True,
"is_overpowering_detected": False,
},
# should pass but because will be also overpowering because previous was overpowering
{
"name": "vtherm4",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
},
],
{"vtherm1": True, "vtherm2": True, "vtherm3": True, "vtherm4": True},
),
# More complex shedding
(
1000,
2000,
[
# already overpowering (non change)
{
"name": "vtherm1",
"device_power": 1100,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
},
# already overpowering and already active (can be un overpowered)
{
"name": "vtherm2",
"device_power": 1100,
"device_power": 5000,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": True,
},
# should terminate the overpowering
{
"name": "vtherm3",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
},
# should terminate the overpowering and active
{
"name": "vtherm4",
"device_power": 3800,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
},
],
{"vtherm2": False, "vtherm3": False, "vtherm4": False},
),
# More complex shedding
(
1000,
2000,
[
# already overpowering (non change)
{
"name": "vtherm1",
"device_power": 1100,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
},
# should be overpowering
{
"name": "vtherm2",
"device_power": 1800,
"is_device_active": False,
"is_over_climate": True,
"is_overpowering_detected": False,
},
# should terminate the overpowering and active but just before is overpowering
{
"name": "vtherm3",
"device_power": 100,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
{"name": "vtherm4", "device_power": 1000, "is_device_active": True, "is_over_climate": True, "is_overpowering_detected": False, "overpowering_state": STATE_OFF},
],
{"vtherm1": False, "vtherm2": True, "vtherm3": True},
# init vtherm1 to False
{"vtherm3": False, "vtherm2": False, "vtherm1": False},
),
# Sheeding only current_power > max_power (need to gain 1000 )
# Shedding
(
2000,
1000,
@@ -432,36 +325,31 @@ async def test_central_power_manageer_find_vtherms(
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
"overpowering_state": STATE_OFF,
},
# should be overpowering but is already
# should be overpowering with many underlmying entities
{
"name": "vtherm2",
"device_power": 600,
"device_power": 400,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"is_overpowering_detected": True,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
# over_climate should be not overpowering (device not active)
# over_climate should be overpowering
{
"name": "vtherm3",
"device_power": 690,
"is_device_active": False,
"is_over_climate": True,
"is_overpowering_detected": False,
},
# over_climate should be overpowering (device active and not already overpowering)
{
"name": "vtherm4",
"device_power": 690,
"device_power": 100,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": False,
"overpowering_state": STATE_OFF,
},
# should not overpower (keep as is)
# should pass cause not active
{
"name": "vtherm5",
"name": "vtherm4",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
@@ -469,8 +357,98 @@ async def test_central_power_manageer_find_vtherms(
"on_percent": 1,
"is_overpowering_detected": False,
},
# should be not overpowering (already overpowering)
{
"name": "vtherm5",
"device_power": 400,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
# should be overpowering with many underluying entities
{
"name": "vtherm6",
"device_power": 400,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
# should not be overpowering (we have enough)
{
"name": "vtherm7",
"device_power": 1000,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
],
{"vtherm1": True, "vtherm4": True},
{"vtherm1": True, "vtherm2": True, "vtherm3": True, "vtherm6": True},
),
# Un-shedding only (will be taken in reverse order)
(
1000,
2000,
[
# should be not unshedded (we have enough)
{
"name": "vtherm0",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
# should be unshedded
{
"name": "vtherm1",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
# already stay unshedded cause already unshedded
{
"name": "vtherm2",
"device_power": 1100,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": False,
"overpowering_state": STATE_OFF,
},
# should be unshedded
{
"name": "vtherm3",
"device_power": 200,
"is_device_active": False,
"is_over_climate": True,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
# should be unshedded
{
"name": "vtherm4",
"device_power": 300,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
],
{"vtherm4": False, "vtherm3": False, "vtherm1": False},
),
],
)
@@ -501,7 +479,10 @@ async def test_central_power_manageer_calculate_shedding(
vtherm.nb_underlying_entities = vtherm_config.get("nb_underlying_entities")
if not vtherm_config.get("is_over_climate"):
vtherm.proportional_algorithm = MagicMock()
vtherm.proportional_algorithm.on_percent = vtherm_config.get("on_percent")
vtherm.on_percent = vtherm.proportional_algorithm.on_percent = vtherm_config.get("on_percent")
else:
vtherm.on_percent = None
vtherm.proportional_algorithm = None
vtherm.power_manager = MagicMock(spec=FeaturePowerManager)
vtherm.power_manager._vtherm = vtherm
@@ -510,6 +491,7 @@ async def test_central_power_manageer_calculate_shedding(
"is_overpowering_detected"
)
vtherm.power_manager.device_power = vtherm_config.get("device_power")
vtherm.power_manager.overpowering_state = vtherm_config.get("overpowering_state")
async def mock_set_overpowering(
overpowering, power_consumption_max=0, v=vtherm

View File

@@ -812,21 +812,20 @@ async def test_multiple_switch_power_management(
assert entity.power_manager.overpowering_state is STATE_OFF
# 2. Send power max mesurement too low and HVACMode is on
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 74))
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
#fmt: off
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
#fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
assert entity.power_percent > 0
# 100 of the device / 4 -> 25, current power 50 so max is 75
await send_max_power_change_event(entity, 74, datetime.now())
await send_max_power_change_event(entity, 49, datetime.now())
assert entity.power_manager.is_overpowering_detected is True
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_POWER
@@ -843,7 +842,7 @@ async def test_multiple_switch_power_management(
"type": "start",
"current_power": 50,
"device_power": 100,
"current_max_power": 74,
"current_max_power": 49,
"current_power_consumption": 100,
},
),

View File

@@ -517,17 +517,18 @@ async def test_power_management_hvac_on(
assert entity.power_manager.overpowering_state is STATE_OFF
# Send power max mesurement too low and HVACMode is on
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149))
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off:
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
# fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await send_max_power_change_event(entity, 149, datetime.now())
await send_max_power_change_event(entity, 49, now)
assert entity.power_manager.is_overpowering_detected is True
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_POWER
@@ -544,7 +545,7 @@ async def test_power_management_hvac_on(
"type": "start",
"current_power": 50,
"device_power": 100,
"current_max_power": 149,
"current_max_power": 49,
"current_power_consumption": 100.0,
},
),
@@ -554,7 +555,7 @@ async def test_power_management_hvac_on(
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 1
# Send power mesurement low to unseet power preset
# Send power mesurement low to unset power preset
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 48))
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
@@ -565,7 +566,7 @@ async def test_power_management_hvac_on(
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await send_power_change_event(entity, 48, datetime.now())
await send_power_change_event(entity, 48, now)
assert entity.power_manager.is_overpowering_detected is False
# All configuration is complete and power is < power_max, we restore previous preset
assert entity.preset_mode is PRESET_BOOST
@@ -582,7 +583,7 @@ async def test_power_management_hvac_on(
"type": "end",
"current_power": 48,
"device_power": 100,
"current_max_power": 149,
"current_max_power": 49,
},
),
],