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
@@ -14,7 +14,11 @@ from akkudoktoreos.core.cache import CacheFileStore
|
||||
from akkudoktoreos.core.coreabc import ConfigMixin, SingletonMixin
|
||||
from akkudoktoreos.core.emplan import ResourceStatus
|
||||
from akkudoktoreos.core.pydantic import ConfigDict, PydanticBaseModel
|
||||
from akkudoktoreos.devices.devicesabc import DevicesBaseSettings
|
||||
from akkudoktoreos.devices.devicesabc import (
|
||||
ConsumerScheduleMode,
|
||||
DevicesBaseSettings,
|
||||
validate_home_appliance_load_definition,
|
||||
)
|
||||
from akkudoktoreos.utils.datetimeutil import DateTime, to_datetime
|
||||
|
||||
# Default charge rates for battery
|
||||
@@ -244,16 +248,77 @@ class InverterCommonSettings(DevicesBaseSettings):
|
||||
|
||||
|
||||
class HomeApplianceCommonSettings(DevicesBaseSettings):
|
||||
"""Home Appliance devices base settings."""
|
||||
"""Flexible consumer (home appliance) devices base settings.
|
||||
|
||||
consumption_wh: int = Field(
|
||||
gt=0, json_schema_extra={"description": "Energy consumption [Wh].", "examples": [2000]}
|
||||
A consumer's load is defined **either** by an explicit power profile
|
||||
(``load_profile_power_w`` with an optional ``load_profile_interval_seconds``)
|
||||
**or** by the flat fallback ``consumption_wh`` + ``duration_h``. Exactly one
|
||||
of the two must be provided.
|
||||
"""
|
||||
|
||||
load_profile_power_w: Optional[list[float]] = Field(
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"Explicit load profile describing a single complete run as a "
|
||||
"sequence of non-negative power values in watts (e.g. "
|
||||
"[200.0, 2000.0, 1800.0, 100.0]). Each value covers "
|
||||
"'load_profile_interval_seconds'. Mutually exclusive with "
|
||||
"consumption_wh/duration_h."
|
||||
),
|
||||
# None-first so the auto-generated config example uses the flat
|
||||
# consumption_wh/duration_h fallback (the two definitions are
|
||||
# mutually exclusive and cannot be shown together).
|
||||
"examples": [None],
|
||||
},
|
||||
)
|
||||
|
||||
duration_h: int = Field(
|
||||
load_profile_interval_seconds: Optional[int] = Field(
|
||||
default=None,
|
||||
gt=0,
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"Duration of one 'load_profile_power_w' step in seconds. Defaults "
|
||||
"to the configured optimization interval when a profile is given."
|
||||
),
|
||||
"examples": [None],
|
||||
},
|
||||
)
|
||||
|
||||
schedule_mode: ConsumerScheduleMode = Field(
|
||||
default=ConsumerScheduleMode.ONCE,
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"Scheduling mode: ONCE (a single run within the horizon) or DAILY "
|
||||
"(one run per local calendar day with a feasible full run)."
|
||||
),
|
||||
"examples": ["ONCE", "DAILY"],
|
||||
},
|
||||
)
|
||||
|
||||
consumption_wh: Optional[int] = Field(
|
||||
default=None,
|
||||
gt=0,
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"Flat fallback: total energy consumption of one run [Wh]. Used "
|
||||
"only when no load_profile_power_w is given."
|
||||
),
|
||||
"examples": [2000],
|
||||
},
|
||||
)
|
||||
|
||||
duration_h: Optional[int] = Field(
|
||||
default=None,
|
||||
gt=0,
|
||||
le=24,
|
||||
json_schema_extra={"description": "Usage duration in hours [0 ... 24].", "examples": [1]},
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"Flat fallback: run duration in hours [0 ... 24]. Used only when "
|
||||
"no load_profile_power_w is given."
|
||||
),
|
||||
"examples": [1],
|
||||
},
|
||||
)
|
||||
|
||||
time_windows: Optional[TimeWindowSequence] = Field(
|
||||
@@ -270,6 +335,17 @@ class HomeApplianceCommonSettings(DevicesBaseSettings):
|
||||
},
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_load_definition(self) -> "HomeApplianceCommonSettings":
|
||||
"""Ensure exactly one complete, valid load definition is provided."""
|
||||
validate_home_appliance_load_definition(
|
||||
load_profile_power_w=self.load_profile_power_w,
|
||||
load_profile_interval_seconds=self.load_profile_interval_seconds,
|
||||
consumption_wh=self.consumption_wh,
|
||||
duration_h=self.duration_h,
|
||||
)
|
||||
return self
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def measurement_keys(self) -> Optional[list[str]]:
|
||||
@@ -341,6 +417,24 @@ class DevicesCommonSettings(SettingsBaseModel):
|
||||
},
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_max_home_appliances(self) -> "DevicesCommonSettings":
|
||||
"""Enforce max_home_appliances purely as an upper bound.
|
||||
|
||||
No demo appliance is created and no on/off behaviour is implied; the
|
||||
limit is only rejected when more appliances are configured than allowed.
|
||||
"""
|
||||
if (
|
||||
self.max_home_appliances is not None
|
||||
and self.home_appliances is not None
|
||||
and len(self.home_appliances) > self.max_home_appliances
|
||||
):
|
||||
raise ValueError(
|
||||
f"Configured {len(self.home_appliances)} home appliances exceeds "
|
||||
f"max_home_appliances = {self.max_home_appliances}."
|
||||
)
|
||||
return self
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def measurement_keys(self) -> Optional[list[str]]:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,11 +1,75 @@
|
||||
"""Flexible consumer (home appliance) device model for genetic optimization.
|
||||
|
||||
A consumer is described by the energy of a **single complete run** resampled onto
|
||||
the optimization slot grid. The optimizer decides, per run, at which slot the run
|
||||
starts; :meth:`HomeAppliance.build_load_curve` then places the resampled run
|
||||
energy at the chosen start(s). Several runs (DAILY mode) and several devices may
|
||||
overlap; their energies simply add up.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from akkudoktoreos.config.configabc import TimeWindow, TimeWindowSequence
|
||||
from akkudoktoreos.config.configabc import TimeWindowSequence
|
||||
from akkudoktoreos.devices.devicesabc import ConsumerScheduleMode
|
||||
from akkudoktoreos.optimization.genetic.geneticdevices import HomeApplianceParameters
|
||||
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration, to_time
|
||||
from akkudoktoreos.utils.datetimeutil import DateTime, to_duration
|
||||
|
||||
|
||||
def resample_power_to_slot_energy(
|
||||
power_w: list[float],
|
||||
input_interval_seconds: float,
|
||||
slot_interval_seconds: float,
|
||||
) -> np.ndarray:
|
||||
"""Resample a piecewise-constant power profile to per-slot energy.
|
||||
|
||||
Each input value ``power_w[i]`` is interpreted as a constant power [W] over
|
||||
the interval ``[i * input_interval_seconds, (i + 1) * input_interval_seconds)``.
|
||||
The energy of every output slot is the time-weighted integral of the input
|
||||
power over that slot::
|
||||
|
||||
E_j = sum_i P_i * overlap(i, j) / 3600 [Wh]
|
||||
|
||||
where ``overlap(i, j)`` is the temporal overlap (in seconds) between input
|
||||
interval ``i`` and output slot ``j``. This is exact for arbitrary (including
|
||||
non-integer) ratios such as 10 -> 15 or 20 -> 15 minutes and conserves
|
||||
energy within numerical tolerance::
|
||||
|
||||
sum_j E_j == sum_i P_i * input_interval_seconds / 3600
|
||||
|
||||
Args:
|
||||
power_w: Piecewise-constant power values [W] of a single run.
|
||||
input_interval_seconds: Duration of one input step [s] (> 0).
|
||||
slot_interval_seconds: Duration of one output slot [s] (> 0).
|
||||
|
||||
Returns:
|
||||
1-D array of per-slot energy [Wh]; length is the number of slots the run
|
||||
occupies (ceil of the total run duration divided by the slot duration).
|
||||
"""
|
||||
n_in = len(power_w)
|
||||
total_seconds = n_in * input_interval_seconds
|
||||
n_slots = int(np.ceil(total_seconds / slot_interval_seconds - 1e-9))
|
||||
out = np.zeros(max(n_slots, 0), dtype=float)
|
||||
for i, power in enumerate(power_w):
|
||||
if power == 0.0:
|
||||
continue
|
||||
seg_start = i * input_interval_seconds
|
||||
seg_end = seg_start + input_interval_seconds
|
||||
first = int(seg_start // slot_interval_seconds)
|
||||
last = int((seg_end - 1e-9) // slot_interval_seconds)
|
||||
for j in range(first, last + 1):
|
||||
slot_start = j * slot_interval_seconds
|
||||
slot_end = slot_start + slot_interval_seconds
|
||||
overlap = min(seg_end, slot_end) - max(seg_start, slot_start)
|
||||
if overlap > 0:
|
||||
out[j] += power * overlap / 3600.0
|
||||
return out
|
||||
|
||||
|
||||
class HomeAppliance:
|
||||
"""A flexible consumer scheduled onto the optimization slot grid."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parameters: HomeApplianceParameters,
|
||||
@@ -13,94 +77,129 @@ class HomeAppliance:
|
||||
prediction_hours: int,
|
||||
slot_duration_h: float = 1.0,
|
||||
):
|
||||
# slot_duration_h is a forward-compatibility hook. Full sub-hourly home
|
||||
# appliance scheduling additionally requires converting the start hour to
|
||||
# a slot index and the duration to a slot count; the default of 1.0 keeps
|
||||
# the hourly behaviour for the default optimization interval of 3600 s.
|
||||
self.parameters: HomeApplianceParameters = parameters
|
||||
self.prediction_hours = prediction_hours
|
||||
self.slot_duration_h = slot_duration_h
|
||||
self._setup()
|
||||
"""Initialize the appliance and precompute its per-slot run energy.
|
||||
|
||||
def _setup(self) -> None:
|
||||
"""Sets up the home appliance parameters based provided parameters."""
|
||||
self.load_curve = np.zeros(self.prediction_hours) # Initialize the load curve with zeros
|
||||
self.duration_h = self.parameters.duration_h
|
||||
self.consumption_wh = self.parameters.consumption_wh
|
||||
# setup possible start times
|
||||
if self.parameters.time_windows is None:
|
||||
self.parameters.time_windows = TimeWindowSequence(
|
||||
windows=[
|
||||
TimeWindow(
|
||||
start_time=to_time("00:00"),
|
||||
duration=to_duration(f"{self.prediction_hours} hours"),
|
||||
),
|
||||
]
|
||||
)
|
||||
start_datetime = to_datetime().set(hour=0, minute=0, second=0)
|
||||
duration = to_duration(f"{self.duration_h} hours")
|
||||
self.start_allowed: list[bool] = []
|
||||
for hour in range(0, self.prediction_hours):
|
||||
self.start_allowed.append(
|
||||
self.parameters.time_windows.contains(
|
||||
start_datetime.add(hours=hour), duration=duration
|
||||
)
|
||||
)
|
||||
start_earliest = self.parameters.time_windows.earliest_start_time(duration, start_datetime)
|
||||
if start_earliest:
|
||||
self.start_earliest = start_earliest.hour
|
||||
else:
|
||||
self.start_earliest = 0
|
||||
start_latest = self.parameters.time_windows.latest_start_time(duration, start_datetime)
|
||||
if start_latest:
|
||||
self.start_latest = start_latest.hour
|
||||
else:
|
||||
self.start_latest = 23
|
||||
|
||||
def set_starting_time(self, start_hour: int, global_start_hour: int = 0) -> int:
|
||||
"""Sets the start time of the device and generates the corresponding load curve.
|
||||
|
||||
:param start_hour: The hour at which the device should start.
|
||||
Args:
|
||||
parameters: Appliance configuration (load definition, schedule mode,
|
||||
allowed time windows).
|
||||
optimization_hours: Optimization horizon in hours (informational).
|
||||
prediction_hours: Total number of optimization slots of the run grid.
|
||||
slot_duration_h: Length of one optimization slot in hours (1.0 hourly,
|
||||
0.25 at 15 min).
|
||||
"""
|
||||
if not self.start_allowed[start_hour]:
|
||||
# It is not allowed (by the time windows) to start the application at this time
|
||||
if global_start_hour <= self.start_latest:
|
||||
# There is a time window left to start the appliance. Use it
|
||||
start_hour = self.start_latest
|
||||
else:
|
||||
# There is no time window left to run the application
|
||||
# Set the start into tomorrow
|
||||
start_hour = self.start_earliest + 24
|
||||
|
||||
self.parameters: HomeApplianceParameters = parameters
|
||||
self.optimization_hours = optimization_hours
|
||||
self.total_slots = int(prediction_hours)
|
||||
self.slot_duration_h = slot_duration_h
|
||||
self.slot_interval_seconds = int(round(slot_duration_h * 3600))
|
||||
self.device_id: str = parameters.device_id
|
||||
self.schedule_mode: ConsumerScheduleMode = parameters.schedule_mode
|
||||
self.time_windows: Optional[TimeWindowSequence] = parameters.time_windows
|
||||
self._build_run_profile()
|
||||
self.reset_load_curve()
|
||||
|
||||
# Calculate power per hour based on total consumption and duration
|
||||
power_per_hour = self.consumption_wh / self.duration_h # Convert to watt-hours
|
||||
def _build_run_profile(self) -> None:
|
||||
"""Build the per-slot energy [Wh] of a single complete run."""
|
||||
if self.parameters.load_profile_power_w is not None:
|
||||
power = [float(value) for value in self.parameters.load_profile_power_w]
|
||||
input_interval = (
|
||||
self.parameters.load_profile_interval_seconds or self.slot_interval_seconds
|
||||
)
|
||||
else:
|
||||
# Flat fallback: constant power over duration_h hours. Route it through
|
||||
# the same resampling path so hourly and sub-hourly grids behave
|
||||
# identically. Power [W] = energy per hour = consumption_wh / duration_h.
|
||||
duration_h = self.parameters.duration_h
|
||||
consumption_wh = self.parameters.consumption_wh
|
||||
power = [consumption_wh / duration_h]
|
||||
input_interval = duration_h * 3600
|
||||
|
||||
# Set the power for the duration of use in the load curve array
|
||||
if start_hour < len(self.load_curve):
|
||||
end_hour = min(start_hour + self.duration_h, self.prediction_hours)
|
||||
self.load_curve[start_hour:end_hour] = power_per_hour
|
||||
self.run_energy_wh: np.ndarray = resample_power_to_slot_energy(
|
||||
power, float(input_interval), float(self.slot_interval_seconds)
|
||||
)
|
||||
self.run_slots: int = int(len(self.run_energy_wh))
|
||||
|
||||
return start_hour
|
||||
def allowed_start_slots(
|
||||
self,
|
||||
*,
|
||||
slot0_datetime: DateTime,
|
||||
earliest_slot: int,
|
||||
horizon_end_slot: int,
|
||||
) -> list[int]:
|
||||
"""Return the sorted absolute start slots at which a full run is allowed.
|
||||
|
||||
A start slot ``s`` is allowed when the complete run fits both the
|
||||
optimization horizon and (if configured) a single allowed time window:
|
||||
|
||||
- ``earliest_slot <= s`` and ``s + run_slots <= horizon_end_slot``
|
||||
- with ``time_windows`` set, the run's whole occupied span starting at
|
||||
``s`` is contained in one window (respecting weekday/date constraints).
|
||||
|
||||
No snapping is performed: every returned slot is a genuinely valid start.
|
||||
|
||||
Args:
|
||||
slot0_datetime: Local, timezone-aware datetime of slot index 0.
|
||||
earliest_slot: First slot the optimizer may schedule at ("now").
|
||||
horizon_end_slot: Exclusive upper bound; a run must end at or before.
|
||||
|
||||
Returns:
|
||||
Sorted list of allowed absolute start slots (may be empty).
|
||||
"""
|
||||
run_slots = self.run_slots
|
||||
if run_slots <= 0:
|
||||
return []
|
||||
last_start = min(horizon_end_slot, self.total_slots) - run_slots
|
||||
first_start = max(earliest_slot, 0)
|
||||
if last_start < first_start:
|
||||
return []
|
||||
|
||||
if self.time_windows is None:
|
||||
return list(range(first_start, last_start + 1))
|
||||
|
||||
run_duration = to_duration(f"{run_slots * self.slot_interval_seconds} seconds")
|
||||
allowed: list[int] = []
|
||||
for slot in range(first_start, last_start + 1):
|
||||
start_dt = slot0_datetime.add(seconds=slot * self.slot_interval_seconds)
|
||||
if self.time_windows.contains(start_dt, duration=run_duration):
|
||||
allowed.append(slot)
|
||||
return allowed
|
||||
|
||||
def build_load_curve(self, starts: list[int]) -> None:
|
||||
"""Place the resampled run energy at each decoded start slot.
|
||||
|
||||
Multiple runs may overlap; their per-slot energies are summed.
|
||||
|
||||
Args:
|
||||
starts: Absolute start slots of the scheduled runs.
|
||||
"""
|
||||
self.reset_load_curve()
|
||||
for start in starts:
|
||||
if start is None or start < 0:
|
||||
continue
|
||||
end = min(start + self.run_slots, self.total_slots)
|
||||
length = end - start
|
||||
if length > 0:
|
||||
self.load_curve[start:end] += self.run_energy_wh[:length]
|
||||
|
||||
def reset_load_curve(self) -> None:
|
||||
"""Resets the load curve."""
|
||||
self.load_curve = np.zeros(self.prediction_hours)
|
||||
"""Reset the load curve to all zeros."""
|
||||
self.load_curve = np.zeros(self.total_slots)
|
||||
|
||||
def get_load_curve(self) -> np.ndarray:
|
||||
"""Returns the current load curve."""
|
||||
"""Return the current per-slot load curve [Wh]."""
|
||||
return self.load_curve
|
||||
|
||||
def get_load_for_hour(self, hour: int) -> float:
|
||||
"""Returns the load for a specific hour.
|
||||
"""Return the load [Wh] for a specific slot.
|
||||
|
||||
:param hour: The hour for which the load is queried.
|
||||
:return: The load in watts for the specified hour.
|
||||
Args:
|
||||
hour: The slot index for which the load is queried.
|
||||
|
||||
Returns:
|
||||
The energy in watt-hours for the specified slot.
|
||||
"""
|
||||
if hour < 0 or hour >= self.prediction_hours:
|
||||
if hour < 0 or hour >= self.total_slots:
|
||||
raise ValueError(
|
||||
f"The specified hour {hour} is outside the available time frame {self.prediction_hours}."
|
||||
f"The specified slot {hour} is outside the available time frame {self.total_slots}."
|
||||
)
|
||||
|
||||
return self.load_curve[hour]
|
||||
|
||||
Reference in New Issue
Block a user