mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-07-20 16:58:12 +00:00
FAstAPI is an async framework. Data may be imported and exported, load and save, set and get asynchronously. Prevent interleaving data operations to corrupt the data. In the previous design sync and async data access was intermixed leading to data corruption. The basic data classes DataSequence and DataContainer and the derived classes like Provider and Measurement now are async. Data access is protected by several async locks. To support the async design of the data classes the database interface became async. The energy management is also adapted to the new async design. Optimization is still off-loaded to another thread, but the prepration for the optimization and the post optimization actions now follow the async design. Adapter operations are now also protected by async locks. Tests were adapted to the async design and new tests were created. Besides this major fix several other improvements and fixes are included in this PR. * fix: key_to_dict/list/array only regard data records with key value set. Before the exclusion of no value data records was only done if the dropna flag was set. * fix: test for visual result pdf generation Due to updates in the library the generated charts text was a little bit different. Adapt the test to create the comaprison pdf in the test data durectory and update the reference pdf. * chore: Remove MutableMapping from DataSequence and DataContainer. Mutable Mapping does not fit to the now async design. * chore: Add NoDB database backend This backend implements the full database backend interface but performs no actual persistence. It is intended for configurations where database persistence is disabled (`provider=None`). * chore: Improve measurement data import testing with real world scenarios. Added two new endpoints to support testing. * chore: Add mermaid to supported documentation tools * chore: Add documentation about async design * chore: Add documentation about generic data handling Covers the basics of measurement and prediction time series data handling. * chore: Add empty lines around markdown lists. * chore: sync pre-commit config to updated package versions Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
283 lines
11 KiB
Python
283 lines
11 KiB
Python
"""Abstract and base classes for predictions.
|
|
|
|
This module provides classes for managing and processing prediction data in a flexible, configurable manner.
|
|
It includes classes to handle configurations, record structures, sequences, and containers for prediction data,
|
|
enabling efficient storage, retrieval, and manipulation of prediction records.
|
|
|
|
This module is designed for use in predictive modeling workflows, facilitating the organization, serialization,
|
|
and manipulation of configuration and prediction data in a clear, scalable, and structured manner.
|
|
"""
|
|
|
|
from typing import List, Optional
|
|
|
|
from loguru import logger
|
|
from pydantic import Field, computed_field
|
|
|
|
from akkudoktoreos.core.coreabc import MeasurementMixin
|
|
from akkudoktoreos.core.dataabc import (
|
|
DataABC,
|
|
DataContainer,
|
|
DataImportProvider,
|
|
DataProvider,
|
|
DataRecord,
|
|
DataSequence,
|
|
)
|
|
from akkudoktoreos.utils.datetimeutil import DateTime, Duration, to_duration
|
|
|
|
|
|
class PredictionABC(DataABC, MeasurementMixin):
|
|
"""Base class for handling prediction data.
|
|
|
|
Enables access to EOS configuration data (attribute `config`) and EOS measurement data
|
|
(attribute `measurement`).
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class PredictionRecord(DataRecord):
|
|
"""Base class for prediction records, enabling dynamic access to fields defined in derived classes.
|
|
|
|
Fields can be accessed and mutated both using dictionary-style access (`record['field_name']`)
|
|
and attribute-style access (`record.field_name`).
|
|
|
|
Attributes:
|
|
date_time (Optional[AwareDatetime]): Aware datetime indicating when the prediction record applies.
|
|
|
|
Configurations:
|
|
- Allows mutation after creation.
|
|
- Supports non-standard data types like `datetime`.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class PredictionSequence(DataSequence):
|
|
"""A managed sequence of PredictionRecord instances with list-like behavior.
|
|
|
|
The PredictionSequence class provides an ordered, mutable collection of PredictionRecord
|
|
instances, allowing list-style access for adding, deleting, and retrieving records. It also
|
|
supports advanced data operations such as JSON serialization, conversion to Pandas Series,
|
|
and sorting by timestamp.
|
|
|
|
Attributes:
|
|
records (List[PredictionRecord]): A list of PredictionRecord instances representing
|
|
individual prediction data points.
|
|
record_keys (Optional[List[str]]): A list of field names (keys) expected in each
|
|
PredictionRecord.
|
|
|
|
Note:
|
|
Derived classes have to provide their own records field with correct record type set.
|
|
|
|
Usage:
|
|
.. code-block:: python
|
|
|
|
# Example of creating, adding, and using PredictionSequence
|
|
class DerivedSequence(PredictionSquence):
|
|
records: List[DerivedPredictionRecord] = Field(default_factory=list, json_schema_extra={ "description": "List of prediction records" })
|
|
|
|
seq = DerivedSequence()
|
|
seq.insert(DerivedPredictionRecord(date_time=datetime.now(), temperature=72))
|
|
seq.insert(DerivedPredictionRecord(date_time=datetime.now(), temperature=75))
|
|
|
|
# Convert to JSON and back
|
|
json_data = seq.to_json()
|
|
new_seq = DerivedSequence.from_json(json_data)
|
|
|
|
# Convert to Pandas Series
|
|
series = seq.key_to_series('temperature')
|
|
|
|
"""
|
|
|
|
# To be overloaded by derived classes.
|
|
records: List[PredictionRecord] = Field(
|
|
default_factory=list, json_schema_extra={"description": "List of prediction records"}
|
|
)
|
|
|
|
|
|
class PredictionStartEndKeepMixin(PredictionABC):
|
|
"""A mixin to manage start, end, and historical retention datetimes for prediction data.
|
|
|
|
The starting datetime for prediction data generation is provided by the energy management
|
|
system. Predictions cannot be computed if this value is `None`.
|
|
"""
|
|
|
|
def historic_hours_min(self) -> int:
|
|
"""Return the minimum historic prediction hours for specific data.
|
|
|
|
To be implemented by derived classes if default 0 is not appropriate.
|
|
"""
|
|
return 0
|
|
|
|
# Computed field for end_datetime and keep_datetime
|
|
@computed_field # type: ignore[prop-decorator]
|
|
@property
|
|
def end_datetime(self) -> Optional[DateTime]:
|
|
"""Compute the end datetime based on the `start_datetime` and `hours`.
|
|
|
|
Ajusts the calculated end time if DST transitions occur within the prediction window.
|
|
|
|
Returns:
|
|
Optional[DateTime]: The calculated end datetime, or `None` if inputs are missing.
|
|
"""
|
|
if self.ems_start_datetime and self.config.prediction.hours:
|
|
end_datetime = self.ems_start_datetime + to_duration(
|
|
f"{self.config.prediction.hours} hours"
|
|
)
|
|
dst_change = end_datetime.offset_hours - self.ems_start_datetime.offset_hours
|
|
logger.debug(
|
|
f"Pre: {self.ems_start_datetime}..{end_datetime}: DST change: {dst_change}"
|
|
)
|
|
if dst_change < 0:
|
|
end_datetime = end_datetime + to_duration(f"{abs(int(dst_change))} hours")
|
|
elif dst_change > 0:
|
|
end_datetime = end_datetime - to_duration(f"{abs(int(dst_change))} hours")
|
|
logger.debug(
|
|
f"Pst: {self.ems_start_datetime}..{end_datetime}: DST change: {dst_change}"
|
|
)
|
|
return end_datetime
|
|
return None
|
|
|
|
@computed_field # type: ignore[prop-decorator]
|
|
@property
|
|
def keep_datetime(self) -> Optional[DateTime]:
|
|
"""Compute the keep datetime for historical data retention.
|
|
|
|
Returns:
|
|
Optional[DateTime]: The calculated retention cutoff datetime, or `None` if inputs are missing.
|
|
"""
|
|
if self.ems_start_datetime is None:
|
|
return None
|
|
historic_hours = self.historic_hours_min()
|
|
if (
|
|
self.config.prediction.historic_hours
|
|
and self.config.prediction.historic_hours > historic_hours
|
|
):
|
|
historic_hours = int(self.config.prediction.historic_hours)
|
|
return self.ems_start_datetime - to_duration(f"{historic_hours} hours")
|
|
|
|
@computed_field # type: ignore[prop-decorator]
|
|
@property
|
|
def total_hours(self) -> Optional[int]:
|
|
"""Compute the hours from `start_datetime` to `end_datetime`.
|
|
|
|
Returns:
|
|
Optional[pendulum.period]: The duration hours, or `None` if either datetime is unavailable.
|
|
"""
|
|
end_dt = self.end_datetime
|
|
if end_dt is None:
|
|
return None
|
|
duration = end_dt - self.ems_start_datetime
|
|
return int(duration.total_hours())
|
|
|
|
@computed_field # type: ignore[prop-decorator]
|
|
@property
|
|
def keep_hours(self) -> Optional[int]:
|
|
"""Compute the hours from `keep_datetime` to `start_datetime`.
|
|
|
|
Returns:
|
|
Optional[pendulum.period]: The duration hours, or `None` if either datetime is unavailable.
|
|
"""
|
|
keep_dt = self.keep_datetime
|
|
if keep_dt is None:
|
|
return None
|
|
duration = self.ems_start_datetime - keep_dt
|
|
return int(duration.total_hours())
|
|
|
|
|
|
class PredictionProvider(PredictionStartEndKeepMixin, DataProvider):
|
|
"""Abstract base class for prediction providers with singleton thread-safety and configurable prediction parameters.
|
|
|
|
This class serves as a base for managing prediction data, providing an interface for derived
|
|
classes to maintain a single instance across threads. It offers attributes for managing
|
|
prediction and historical data retention.
|
|
|
|
Note:
|
|
Derived classes have to provide their own records field with correct record type set.
|
|
"""
|
|
|
|
def db_keep_datetime(self) -> Optional[DateTime]:
|
|
"""Earliest datetime from which database records should be retained.
|
|
|
|
Used when removing old records from database to free space.
|
|
|
|
Subclasses may override this method to provide a domain-specific default.
|
|
|
|
Returns:
|
|
Datetime or None.
|
|
"""
|
|
return self.keep_datetime
|
|
|
|
def db_initial_time_window(self) -> Optional[Duration]:
|
|
"""Return the initial time window used for database loading.
|
|
|
|
This window defines the initial symmetric time span around a target datetime
|
|
that should be loaded from the database when no explicit search time window
|
|
is specified. It serves as a loading hint and may be expanded by the caller
|
|
if no records are found within the initial range.
|
|
|
|
Subclasses may override this method to provide a domain-specific default.
|
|
|
|
Returns:
|
|
The initial loading time window as a Duration, or ``None`` to indicate
|
|
that no initial window constraint should be applied.
|
|
"""
|
|
hours = max(self.config.prediction.hours, self.config.prediction_historic_hours, 24)
|
|
return to_duration(hours * 3600)
|
|
|
|
async def update_data(
|
|
self,
|
|
force_enable: Optional[bool] = False,
|
|
force_update: Optional[bool] = False,
|
|
) -> None:
|
|
"""Update prediction parameters and call the custom update function.
|
|
|
|
Updates the configuration, deletes outdated records, and performs the custom update logic.
|
|
|
|
Args:
|
|
force_enable (bool, optional): If True, forces the update even if the provider is disabled.
|
|
force_update (bool, optional): If True, forces the provider to update the data even if still cached.
|
|
"""
|
|
# Check after configuration is updated.
|
|
if not force_enable and not self.enabled():
|
|
return
|
|
|
|
# Delete outdated records before updating
|
|
await self.delete_by_datetime(end_datetime=self.keep_datetime)
|
|
|
|
# Call the custom update logic
|
|
await self._update_data(force_update=force_update)
|
|
|
|
|
|
class PredictionImportProvider(PredictionProvider, DataImportProvider):
|
|
"""Abstract base class for prediction providers that import prediction data.
|
|
|
|
This class is designed to handle prediction data provided in the form of a key-value dictionary.
|
|
|
|
- **Keys**: Represent identifiers from the record keys of a specific prediction.
|
|
- **Values**: Are lists of prediction values starting at a specified `start_datetime`, where
|
|
each value corresponds to a subsequent time interval (e.g., hourly).
|
|
|
|
Subclasses must implement the logic for managing prediction data based on the imported records.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class PredictionContainer(PredictionStartEndKeepMixin, DataContainer):
|
|
"""A container for managing multiple PredictionProvider instances.
|
|
|
|
This class enables access to data from multiple prediction providers, supporting retrieval and
|
|
aggregation of their data as Pandas Series objects. It acts as a dictionary-like structure
|
|
where each key represents a specific data field, and the value is a Pandas Series containing
|
|
combined data from all PredictionProvider instances for that key.
|
|
|
|
Note:
|
|
Derived classes have to provide their own providers field with correct provider type set.
|
|
"""
|
|
|
|
# To be overloaded by derived classes.
|
|
providers: List[PredictionProvider] = Field(
|
|
default_factory=list, json_schema_extra={"description": "List of prediction providers"}
|
|
)
|