mirror of
				https://github.com/Akkudoktor-EOS/EOS.git
				synced 2025-11-04 08:46:20 +00:00 
			
		
		
		
	Fix config and prediction revamp. (#259)
Extend single_test_optimization.py to be able to use real world data from new prediction classes. - .venv/bin/python single_test_optimization.py --real_world --verbose Can also be run with profiling "--profile". Add single_test_prediction.py to fetch predictions from remote prediction providers - .venv/bin/python single_test_prediction.py --verbose --provider-id PVForecastAkkudoktor | more - .venv/bin/python single_test_prediction.py --verbose --provider-id LoadAkkudoktor | more - .venv/bin/python single_test_prediction.py --verbose --provider-id ElecPriceAkkudoktor | more - .venv/bin/python single_test_prediction.py --verbose --provider-id BrightSky | more - .venv/bin/python single_test_prediction.py --verbose --provider-id ClearOutside | more Can also be run with profiling "--profile". single_test_optimization.py is an example on how to retrieve prediction data for optimization and use it to set up the optimization parameters. Changes: - load: Only one load provider at a time (vs. 5 before) Bug fixes: - prediction: only use providers that are enabled to retrieve or set data. - prediction: fix pre pendulum format strings - dataabc: Prevent error when resampling data with no datasets. Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
		@@ -7,5 +7,5 @@ from akkudoktoreos.config.configabc import SettingsBaseModel
 | 
			
		||||
 | 
			
		||||
class ElecPriceCommonSettings(SettingsBaseModel):
 | 
			
		||||
    elecprice_provider: Optional[str] = Field(
 | 
			
		||||
        "ElecPriceAkkudoktor", description="Electicity price provider id of provider to be used."
 | 
			
		||||
        default=None, description="Electicity price provider id of provider to be used."
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,7 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def provider_id(cls) -> str:
 | 
			
		||||
        """Return the unique identifier for the Akkudoktor provider."""
 | 
			
		||||
        return "Akkudoktor"
 | 
			
		||||
        return "ElecPriceAkkudoktor"
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def _validate_data(cls, json_str: Union[bytes, Any]) -> AkkudoktorElecPrice:
 | 
			
		||||
@@ -98,8 +98,8 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
 | 
			
		||||
            ValueError: If the API response does not include expected `electricity price` data.
 | 
			
		||||
        """
 | 
			
		||||
        source = "https://api.akkudoktor.net"
 | 
			
		||||
        date = to_datetime(self.start_datetime, as_string="%Y-%m-%d")
 | 
			
		||||
        last_date = to_datetime(self.end_datetime, as_string="%Y-%m-%d")
 | 
			
		||||
        date = to_datetime(self.start_datetime, as_string="Y-M-D")
 | 
			
		||||
        last_date = to_datetime(self.end_datetime, as_string="Y-M-D")
 | 
			
		||||
        response = requests.get(
 | 
			
		||||
            f"{source}/prices?date={date}&last_date={last_date}&tz={self.config.timezone}"
 | 
			
		||||
        )
 | 
			
		||||
@@ -146,6 +146,10 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
 | 
			
		||||
                elecprice_marketprice=akkudoktor_data.values[i].marketpriceEurocentPerKWh,
 | 
			
		||||
            )
 | 
			
		||||
            self.append(record)
 | 
			
		||||
        if len(self) == 0:
 | 
			
		||||
            # Got no valid forecast data
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Assure price starts at start_time
 | 
			
		||||
        if compare_datetimes(self[0].date_time, self.start_datetime).gt:
 | 
			
		||||
            record = ElecPriceDataRecord(
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ logger = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ElecPriceImportCommonSettings(SettingsBaseModel):
 | 
			
		||||
    """Common settings for elecprice data import from file."""
 | 
			
		||||
    """Common settings for elecprice data import from file or JSON String."""
 | 
			
		||||
 | 
			
		||||
    elecpriceimport_file_path: Optional[Union[str, Path]] = Field(
 | 
			
		||||
        default=None, description="Path to the file to import elecprice data from."
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
"""Load forecast module for load predictions."""
 | 
			
		||||
 | 
			
		||||
from typing import Optional, Set
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from pydantic import Field, computed_field
 | 
			
		||||
from pydantic import Field
 | 
			
		||||
 | 
			
		||||
from akkudoktoreos.config.configabc import SettingsBaseModel
 | 
			
		||||
from akkudoktoreos.utils.logutil import get_logger
 | 
			
		||||
@@ -12,50 +12,7 @@ logger = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
class LoadCommonSettings(SettingsBaseModel):
 | 
			
		||||
    # Load 0
 | 
			
		||||
    load0_provider: Optional[str] = Field(
 | 
			
		||||
    load_provider: Optional[str] = Field(
 | 
			
		||||
        default=None, description="Load provider id of provider to be used."
 | 
			
		||||
    )
 | 
			
		||||
    load0_name: Optional[str] = Field(default=None, description="Name of the load source.")
 | 
			
		||||
 | 
			
		||||
    # Load 1
 | 
			
		||||
    load1_provider: Optional[str] = Field(
 | 
			
		||||
        default=None, description="Load provider id of provider to be used."
 | 
			
		||||
    )
 | 
			
		||||
    load1_name: Optional[str] = Field(default=None, description="Name of the load source.")
 | 
			
		||||
 | 
			
		||||
    # Load 2
 | 
			
		||||
    load2_provider: Optional[str] = Field(
 | 
			
		||||
        default=None, description="Load provider id of provider to be used."
 | 
			
		||||
    )
 | 
			
		||||
    load2_name: Optional[str] = Field(default=None, description="Name of the load source.")
 | 
			
		||||
 | 
			
		||||
    # Load 3
 | 
			
		||||
    load3_provider: Optional[str] = Field(
 | 
			
		||||
        default=None, description="Load provider id of provider to be used."
 | 
			
		||||
    )
 | 
			
		||||
    load3_name: Optional[str] = Field(default=None, description="Name of the load source.")
 | 
			
		||||
 | 
			
		||||
    # Load 4
 | 
			
		||||
    load4_provider: Optional[str] = Field(
 | 
			
		||||
        default=None, description="Load provider id of provider to be used."
 | 
			
		||||
    )
 | 
			
		||||
    load4_name: Optional[str] = Field(default=None, description="Name of the load source.")
 | 
			
		||||
 | 
			
		||||
    # Computed fields
 | 
			
		||||
    @computed_field  # type: ignore[prop-decorator]
 | 
			
		||||
    @property
 | 
			
		||||
    def load_count(self) -> int:
 | 
			
		||||
        """Maximum number of loads."""
 | 
			
		||||
        return 5
 | 
			
		||||
 | 
			
		||||
    @computed_field  # type: ignore[prop-decorator]
 | 
			
		||||
    @property
 | 
			
		||||
    def load_providers(self) -> Set[str]:
 | 
			
		||||
        """Load providers."""
 | 
			
		||||
        providers = []
 | 
			
		||||
        for i in range(self.load_count):
 | 
			
		||||
            load_provider_attr = f"load{i}_provider"
 | 
			
		||||
            value = getattr(self, load_provider_attr)
 | 
			
		||||
            if value:
 | 
			
		||||
                providers.append(value)
 | 
			
		||||
        return set(providers)
 | 
			
		||||
    load_name: Optional[str] = Field(default=None, description="Name of the load source.")
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ Notes:
 | 
			
		||||
from abc import abstractmethod
 | 
			
		||||
from typing import List, Optional
 | 
			
		||||
 | 
			
		||||
from pydantic import Field, computed_field
 | 
			
		||||
from pydantic import Field
 | 
			
		||||
 | 
			
		||||
from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord
 | 
			
		||||
from akkudoktoreos.utils.logutil import get_logger
 | 
			
		||||
@@ -18,41 +18,8 @@ logger = get_logger(__name__)
 | 
			
		||||
class LoadDataRecord(PredictionRecord):
 | 
			
		||||
    """Represents a load data record containing various load attributes at a specific datetime."""
 | 
			
		||||
 | 
			
		||||
    load0_mean: Optional[float] = Field(default=None, description="Load 0 mean value (W)")
 | 
			
		||||
    load0_std: Optional[float] = Field(default=None, description="Load 0 standard deviation (W)")
 | 
			
		||||
    load1_mean: Optional[float] = Field(default=None, description="Load 1 mean value (W)")
 | 
			
		||||
    load1_std: Optional[float] = Field(default=None, description="Load 1 standard deviation (W)")
 | 
			
		||||
    load2_mean: Optional[float] = Field(default=None, description="Load 2 mean value (W)")
 | 
			
		||||
    load2_std: Optional[float] = Field(default=None, description="Load 2 standard deviation (W)")
 | 
			
		||||
    load3_mean: Optional[float] = Field(default=None, description="Load 3 mean value (W)")
 | 
			
		||||
    load3_std: Optional[float] = Field(default=None, description="Load 3 standard deviation (W)")
 | 
			
		||||
    load4_mean: Optional[float] = Field(default=None, description="Load 4 mean value (W)")
 | 
			
		||||
    load4_std: Optional[float] = Field(default=None, description="Load 4 standard deviation (W)")
 | 
			
		||||
 | 
			
		||||
    # Computed fields
 | 
			
		||||
    @computed_field  # type: ignore[prop-decorator]
 | 
			
		||||
    @property
 | 
			
		||||
    def load_total_mean(self) -> float:
 | 
			
		||||
        """Total load mean value (W)."""
 | 
			
		||||
        total_mean = 0.0
 | 
			
		||||
        for i in range(5):
 | 
			
		||||
            load_mean_attr = f"load{i}_mean"
 | 
			
		||||
            value = getattr(self, load_mean_attr)
 | 
			
		||||
            if value:
 | 
			
		||||
                total_mean += value
 | 
			
		||||
        return total_mean
 | 
			
		||||
 | 
			
		||||
    @computed_field  # type: ignore[prop-decorator]
 | 
			
		||||
    @property
 | 
			
		||||
    def load_total_std(self) -> float:
 | 
			
		||||
        """Total load standard deviation (W)."""
 | 
			
		||||
        total_std = 0.0
 | 
			
		||||
        for i in range(5):
 | 
			
		||||
            load_std_attr = f"load{i}_std"
 | 
			
		||||
            value = getattr(self, load_std_attr)
 | 
			
		||||
            if value:
 | 
			
		||||
                total_std += value
 | 
			
		||||
        return total_std
 | 
			
		||||
    load_mean: Optional[float] = Field(default=None, description="Load mean value (W)")
 | 
			
		||||
    load_std: Optional[float] = Field(default=None, description="Load standard deviation (W)")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoadProvider(PredictionProvider):
 | 
			
		||||
@@ -86,17 +53,4 @@ class LoadProvider(PredictionProvider):
 | 
			
		||||
        return "LoadProvider"
 | 
			
		||||
 | 
			
		||||
    def enabled(self) -> bool:
 | 
			
		||||
        logger.debug(
 | 
			
		||||
            f"LoadProvider ID {self.provider_id()} vs. config {self.config.load_providers}"
 | 
			
		||||
        )
 | 
			
		||||
        return self.provider_id() == self.config.load_providers
 | 
			
		||||
 | 
			
		||||
    def loads(self) -> List[str]:
 | 
			
		||||
        """Returns a list of key prefixes of the loads managed by this provider."""
 | 
			
		||||
        loads_prefix = []
 | 
			
		||||
        for i in range(self.config.load_count):
 | 
			
		||||
            load_provider_attr = f"load{i}_provider"
 | 
			
		||||
            value = getattr(self.config, load_provider_attr)
 | 
			
		||||
            if value == self.provider_id():
 | 
			
		||||
                loads_prefix.append(f"load{i}")
 | 
			
		||||
        return loads_prefix
 | 
			
		||||
        return self.provider_id() == self.config.load_provider
 | 
			
		||||
 
 | 
			
		||||
@@ -39,8 +39,8 @@ class LoadAkkudoktor(LoadProvider):
 | 
			
		||||
            profile_data = np.array(
 | 
			
		||||
                list(zip(file_data["yearly_profiles"], file_data["yearly_profiles_std"]))
 | 
			
		||||
            )
 | 
			
		||||
            data_year_energy = profile_data * self.config.loadakkudoktor_year_energy
 | 
			
		||||
            # pprint(self.data_year_energy)
 | 
			
		||||
            # Calculate values in W by relative profile data and yearly consumption given in kWh
 | 
			
		||||
            data_year_energy = profile_data * self.config.loadakkudoktor_year_energy * 1000
 | 
			
		||||
        except FileNotFoundError:
 | 
			
		||||
            error_msg = f"Error: File {load_file} not found."
 | 
			
		||||
            logger.error(error_msg)
 | 
			
		||||
@@ -54,16 +54,13 @@ class LoadAkkudoktor(LoadProvider):
 | 
			
		||||
    def _update_data(self, force_update: Optional[bool] = False) -> None:
 | 
			
		||||
        """Adds the load means and standard deviations."""
 | 
			
		||||
        data_year_energy = self.load_data()
 | 
			
		||||
        for load in self.loads():
 | 
			
		||||
            attr_load_mean = f"{load}_mean"
 | 
			
		||||
            attr_load_std = f"{load}_std"
 | 
			
		||||
            date = self.start_datetime
 | 
			
		||||
            for i in range(self.config.prediction_hours):
 | 
			
		||||
                # Extract mean and standard deviation for the given day and hour
 | 
			
		||||
                # Day indexing starts at 0, -1 because of that
 | 
			
		||||
                hourly_stats = data_year_energy[date.day_of_year - 1, :, date.hour]
 | 
			
		||||
                self.update_value(date, attr_load_mean, hourly_stats[0])
 | 
			
		||||
                self.update_value(date, attr_load_std, hourly_stats[1])
 | 
			
		||||
                date += to_duration("1 hour")
 | 
			
		||||
        date = self.start_datetime
 | 
			
		||||
        for i in range(self.config.prediction_hours):
 | 
			
		||||
            # Extract mean and standard deviation for the given day and hour
 | 
			
		||||
            # Day indexing starts at 0, -1 because of that
 | 
			
		||||
            hourly_stats = data_year_energy[date.day_of_year - 1, :, date.hour]
 | 
			
		||||
            self.update_value(date, "load_mean", hourly_stats[0])
 | 
			
		||||
            self.update_value(date, "load_std", hourly_stats[1])
 | 
			
		||||
            date += to_duration("1 hour")
 | 
			
		||||
        # We are working on fresh data (no cache), report update time
 | 
			
		||||
        self.update_datetime = to_datetime(in_timezone=self.config.timezone)
 | 
			
		||||
 
 | 
			
		||||
@@ -20,48 +20,17 @@ logger = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoadImportCommonSettings(SettingsBaseModel):
 | 
			
		||||
    """Common settings for load data import from file."""
 | 
			
		||||
    """Common settings for load data import from file or JSON string."""
 | 
			
		||||
 | 
			
		||||
    load0_import_file_path: Optional[Union[str, Path]] = Field(
 | 
			
		||||
    load_import_file_path: Optional[Union[str, Path]] = Field(
 | 
			
		||||
        default=None, description="Path to the file to import load data from."
 | 
			
		||||
    )
 | 
			
		||||
    load0_import_json: Optional[str] = Field(
 | 
			
		||||
        default=None, description="JSON string, dictionary of load forecast value lists."
 | 
			
		||||
    )
 | 
			
		||||
    load1_import_file_path: Optional[Union[str, Path]] = Field(
 | 
			
		||||
        default=None, description="Path to the file to import load data from."
 | 
			
		||||
    )
 | 
			
		||||
    load1_import_json: Optional[str] = Field(
 | 
			
		||||
        default=None, description="JSON string, dictionary of load forecast value lists."
 | 
			
		||||
    )
 | 
			
		||||
    load2_import_file_path: Optional[Union[str, Path]] = Field(
 | 
			
		||||
        default=None, description="Path to the file to import load data from."
 | 
			
		||||
    )
 | 
			
		||||
    load2_import_json: Optional[str] = Field(
 | 
			
		||||
        default=None, description="JSON string, dictionary of load forecast value lists."
 | 
			
		||||
    )
 | 
			
		||||
    load3_import_file_path: Optional[Union[str, Path]] = Field(
 | 
			
		||||
        default=None, description="Path to the file to import load data from."
 | 
			
		||||
    )
 | 
			
		||||
    load3_import_json: Optional[str] = Field(
 | 
			
		||||
        default=None, description="JSON string, dictionary of load forecast value lists."
 | 
			
		||||
    )
 | 
			
		||||
    load4_import_file_path: Optional[Union[str, Path]] = Field(
 | 
			
		||||
        default=None, description="Path to the file to import load data from."
 | 
			
		||||
    )
 | 
			
		||||
    load4_import_json: Optional[str] = Field(
 | 
			
		||||
    load_import_json: Optional[str] = Field(
 | 
			
		||||
        default=None, description="JSON string, dictionary of load forecast value lists."
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Validators
 | 
			
		||||
    @field_validator(
 | 
			
		||||
        "load0_import_file_path",
 | 
			
		||||
        "load1_import_file_path",
 | 
			
		||||
        "load2_import_file_path",
 | 
			
		||||
        "load3_import_file_path",
 | 
			
		||||
        "load4_import_file_path",
 | 
			
		||||
        mode="after",
 | 
			
		||||
    )
 | 
			
		||||
    @field_validator("load_import_file_path", mode="after")
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def validate_loadimport_file_path(cls, value: Optional[Union[str, Path]]) -> Optional[Path]:
 | 
			
		||||
        if value is None:
 | 
			
		||||
@@ -89,12 +58,7 @@ class LoadImport(LoadProvider, PredictionImportProvider):
 | 
			
		||||
        return "LoadImport"
 | 
			
		||||
 | 
			
		||||
    def _update_data(self, force_update: Optional[bool] = False) -> None:
 | 
			
		||||
        for load in self.loads():
 | 
			
		||||
            attr_file_path = f"{load}_import_file_path"
 | 
			
		||||
            attr_json = f"{load}_import_json"
 | 
			
		||||
            import_file_path = getattr(self.config, attr_file_path)
 | 
			
		||||
            if import_file_path is not None:
 | 
			
		||||
                self.import_from_file(import_file_path, key_prefix=load)
 | 
			
		||||
            import_json = getattr(self.config, attr_json)
 | 
			
		||||
            if import_json is not None:
 | 
			
		||||
                self.import_from_json(import_json, key_prefix=load)
 | 
			
		||||
        if self.config.load_import_file_path is not None:
 | 
			
		||||
            self.import_from_file(self.config.load_import_file_path, key_prefix="load")
 | 
			
		||||
        if self.config.load_import_json is not None:
 | 
			
		||||
            self.import_from_json(self.config.load_import_json, key_prefix="load")
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ logger = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PVForecastImportCommonSettings(SettingsBaseModel):
 | 
			
		||||
    """Common settings for pvforecast data import from file."""
 | 
			
		||||
    """Common settings for pvforecast data import from file or JSON string."""
 | 
			
		||||
 | 
			
		||||
    pvforecastimport_file_path: Optional[Union[str, Path]] = Field(
 | 
			
		||||
        default=None, description="Path to the file to import pvforecast data from."
 | 
			
		||||
 
 | 
			
		||||
@@ -9,5 +9,5 @@ from akkudoktoreos.config.configabc import SettingsBaseModel
 | 
			
		||||
 | 
			
		||||
class WeatherCommonSettings(SettingsBaseModel):
 | 
			
		||||
    weather_provider: Optional[str] = Field(
 | 
			
		||||
        default="ClearOutside", description="Weather provider id of provider to be used."
 | 
			
		||||
        default=None, description="Weather provider id of provider to be used."
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -96,8 +96,8 @@ class WeatherBrightSky(WeatherProvider):
 | 
			
		||||
            ValueError: If the API response does not include expected `weather` data.
 | 
			
		||||
        """
 | 
			
		||||
        source = "https://api.brightsky.dev"
 | 
			
		||||
        date = to_datetime(self.start_datetime, as_string="%Y-%m-%d")
 | 
			
		||||
        last_date = to_datetime(self.end_datetime, as_string="%Y-%m-%d")
 | 
			
		||||
        date = to_datetime(self.start_datetime, as_string="Y-M-D")
 | 
			
		||||
        last_date = to_datetime(self.end_datetime, as_string="Y-M-D")
 | 
			
		||||
        response = requests.get(
 | 
			
		||||
            f"{source}/weather?lat={self.config.latitude}&lon={self.config.longitude}&date={date}&last_date={last_date}&tz={self.config.timezone}"
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ logger = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WeatherImportCommonSettings(SettingsBaseModel):
 | 
			
		||||
    """Common settings for weather data import from file."""
 | 
			
		||||
    """Common settings for weather data import from file or JSON string."""
 | 
			
		||||
 | 
			
		||||
    weatherimport_file_path: Optional[Union[str, Path]] = Field(
 | 
			
		||||
        default=None, description="Path to the file to import weather data from."
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user