mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-08-31 12:46:38 +00:00
feat(optimization): schedule any number of flexible consumers
Replace the single hourly "dishwasher" home appliance with a list of flexible consumers (home_appliances). Each consumer defines its load either as an explicit power profile (energy-preservingly resampled onto the optimization slot grid, incl. 15-min and non-integer interval ratios) or the flat consumption_wh/duration_h fallback, and runs ONCE or DAILY within its time windows and the optimization horizon. - ConsumerScheduleMode + shared load-definition validation (XOR of profile/fallback, reject negative/NaN/inf, unique device_id) - ApplianceGeneLayout: variable appliance gene block (index into allowed_start_slots), ONCE/DAILY calendar-day based, no snapping - per-device output: result.home_appliance_energy_wh, appliance_starts (absolute local times), per-device solution columns and DDBC RUN/OFF instructions on state transitions only - deprecate dishwasher/washingstart/Home_appliance_wh_per_hour with backward-compatible mapping and explicit conflict rejection - max_home_appliances is now an upper bound only; no demo appliance and no on/off behaviour - docs, openapi.json, CHANGELOG and optimize_result_2* fixtures updated; new tests/test_homeappliance.py covers the mandatory test matrix Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c59bf1b486
commit
67cf6f7d8a
@@ -1,6 +1,8 @@
|
||||
"""Abstract and base classes for devices."""
|
||||
|
||||
import math
|
||||
from enum import StrEnum
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@@ -87,6 +89,90 @@ class BatteryOperationMode(StrEnum):
|
||||
FAULT = "FAULT"
|
||||
|
||||
|
||||
def validate_home_appliance_load_definition(
|
||||
*,
|
||||
load_profile_power_w: Optional[list[float]],
|
||||
load_profile_interval_seconds: Optional[int],
|
||||
consumption_wh: Optional[float],
|
||||
duration_h: Optional[float],
|
||||
) -> None:
|
||||
"""Validate the load definition of a flexible consumer / home appliance.
|
||||
|
||||
A consumer's load must be given **either** as a full explicit power profile
|
||||
(``load_profile_power_w``) **or** as the complete flat fallback
|
||||
(``consumption_wh`` together with ``duration_h``). Providing both, or only a
|
||||
part of the fallback, is rejected. Profile values must be finite and
|
||||
non-negative and the profile interval, if given, must be positive.
|
||||
|
||||
Args:
|
||||
load_profile_power_w: Explicit per-step power values [W], or None.
|
||||
load_profile_interval_seconds: Duration of one profile step [s], or None.
|
||||
consumption_wh: Fallback total energy of one run [Wh], or None.
|
||||
duration_h: Fallback run duration [h], or None.
|
||||
|
||||
Raises:
|
||||
ValueError: If the definition is conflicting, incomplete, or contains
|
||||
invalid profile values.
|
||||
"""
|
||||
profile_given = load_profile_power_w is not None
|
||||
fallback_fields = (consumption_wh, duration_h)
|
||||
fallback_partial = any(field is not None for field in fallback_fields)
|
||||
fallback_given = all(field is not None for field in fallback_fields)
|
||||
|
||||
if profile_given and fallback_partial:
|
||||
raise ValueError(
|
||||
"Conflicting home appliance load definition: provide either "
|
||||
"load_profile_power_w or consumption_wh together with duration_h, "
|
||||
"not both."
|
||||
)
|
||||
|
||||
if not profile_given:
|
||||
if not fallback_given:
|
||||
raise ValueError(
|
||||
"Incomplete home appliance load definition: provide a full "
|
||||
"load_profile_power_w or both consumption_wh and duration_h."
|
||||
)
|
||||
# Value ranges of the fallback fields are enforced by their Field
|
||||
# constraints (gt=0); nothing more to check here.
|
||||
return
|
||||
|
||||
# Explicit profile path.
|
||||
if load_profile_interval_seconds is not None and load_profile_interval_seconds <= 0:
|
||||
raise ValueError("load_profile_interval_seconds must be greater than zero.")
|
||||
if len(load_profile_power_w) == 0:
|
||||
raise ValueError("load_profile_power_w must not be empty.")
|
||||
for value in load_profile_power_w:
|
||||
if value is None or math.isnan(value) or math.isinf(value):
|
||||
raise ValueError(
|
||||
"load_profile_power_w must contain only finite values "
|
||||
"(no NaN or infinity)."
|
||||
)
|
||||
if value < 0:
|
||||
raise ValueError("load_profile_power_w must not contain negative values.")
|
||||
|
||||
|
||||
class ConsumerScheduleMode(StrEnum):
|
||||
"""Schedule mode of a flexible consumer (home appliance).
|
||||
|
||||
Determines how often a consumer's load profile is scheduled within the
|
||||
optimization horizon.
|
||||
|
||||
Modes
|
||||
-----
|
||||
- ONCE:
|
||||
The consumer runs exactly once somewhere within the optimization
|
||||
horizon ("fire and forget"). The optimizer picks the start.
|
||||
|
||||
- DAILY:
|
||||
The consumer runs once per local calendar day, but only on days for
|
||||
which at least one complete, allowed run still fits into the remaining
|
||||
horizon. The optimizer picks one start per eligible day.
|
||||
"""
|
||||
|
||||
ONCE = "ONCE"
|
||||
DAILY = "DAILY"
|
||||
|
||||
|
||||
class ApplianceOperationMode(StrEnum):
|
||||
"""Appliance operation modes.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user