Files
EOS/src/akkudoktoreos/prediction/elecfeeabc.py
T
1abdd345c4 fix: unify mypy environments for local checks and CI (#1291)
The isolated pre-commit mypy hook previously omitted runtime type information that
make mypy used, hiding errors involving dependencies such as Pydantic and Pendulum.
Makefile, pre-commit and CI now run the same full-project typing policy in the
development environment defined by uv.lock.

- Use uv run --locked --exact --extra dev and the same mypy arguments for Makefile
  and the local hook. Check all of src and tests, including on configuration-only
  changes.
- Pin Python 3.13 for local development and the pre-commit CI job, and install the
  locked pre-commit version in CI.
- Disable incremental analysis because existing Pendulum cache state changes mypy 2.3.1
  diagnostics. Document the policy, the performance tradeoff and the existing typing debt.
- Add a regression test that exercises Makefile, the hook and the CI command in a
  temporary project, accepting valid dependency types and detecting deliberate
  Pydantic/Pendulum assignment errors.

Resolve the newly detected mypy diagnostics.

- Enable the numpydantic and Pydantic mypy plugins, retaining strict Pydantic
  constructor typing with init_typed = true. Validate raw/coercible payloads through model_validate.
- Propagate concrete record, provider and time-window types through generic collections,
  factories and lookup methods. Preserve runtime field inspection and generated time-window
  documentation.
- Align Pendulum annotations with actual factory/arithmetic results while retaining Pydantic
  validation adapters at runtime. Correct optional values, array boundaries, REST handlers
  and plotting interfaces.
- Add pinned scipy-stubs and types-psutil, update uv.lock, and supply the plugins' dependencies.
- Add runtime regression coverage for validated path defaults, normalized time-series metadata,
  generic field inspection, invalid timestamps and unsupported provider imports.

Runtime and compatibility details:

- Validate path defaults as Path objects while retaining raw string defaults needed by
  migration serialization with exclude_defaults.
- Normalize feed-in tariff lists and default charge rates to NumPy arrays; reject missing
  timestamps/uninitialized values explicitly. Importing into a provider without import support
  returns HTTP 400.
- Public JSON schemas and OpenAPI structure match main (excluding the generated version).

Signed-off-by: dr-dimitry

Signed-off-by: dr-dimitry
Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
Co-authored-by: dr-dimitri <87113560+dr-dimitri@users.noreply.github.com>
Co-authored-by: Normann <github@koldrack.com>
2026-09-10 23:20:35 +02:00

129 lines
4.8 KiB
Python

"""Abstract and base classes for electricity fee predictions.
Notes:
- Ensure appropriate API keys or configurations are set up if required by external data sources.
"""
from abc import abstractmethod
from typing import List, Optional
from pydantic import Field, computed_field
from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord
class ElecFeeDataRecord(PredictionRecord):
"""Represents a electricity price data record containing various price attributes at a specific datetime.
Attributes:
date_time (Optional[AwareDatetime]): The datetime of the record.
"""
elecfee_consumption_amt_wh: Optional[float] = Field(
None,
json_schema_extra={
"description": (
"Total fixed fee for consumed energy per Wh [amount/Wh]. "
"This is the accumulation of all fixed per-Wh fees payable on "
"consumed energy - such as network charge, concession fee, "
"and electricity charge - into a single amount."
),
},
)
elecfee_consumption_percent_amt: Optional[float] = Field(
None,
json_schema_extra={
"description": (
"Total fixed surcharge on consumed energy, given as a "
"percentage of the monetary amount already charged for that "
"energy [%]. This is the accumulation of all percentage-based "
"surcharges payable on top of the consumed-energy fee - such "
"as VAT - into a single percentage. This is a percentage of "
"the fee amount, not a per-Wh rate."
),
},
)
elecfee_feedin_amt_wh: Optional[float] = Field(
None,
json_schema_extra={
"description": (
"Total fixed deduction from feed-in energy per Wh [amount/Wh]. "
"This is the accumulation of all fixed per-Wh charges deducted from "
"feed-in energy - such as metering fees or grid-operator handling "
"charges - into a single amount. Applied after the percentage-based "
"deduction, i.e. it reduces the price by a flat amount per Wh "
"rather than by a share of the raw price."
),
},
)
elecfee_feedin_percent_amt: Optional[float] = Field(
None,
json_schema_extra={
"description": (
"Total percentage deducted from the raw feed-in price (spot price) "
"[%]. This is the accumulation of all percentage-based deductions "
"payable on the feed-in tariff - such as a marketing or balancing "
"fee retained by the aggregator - into a single percentage. It is "
"applied as `raw_price * (100 - percent) / 100`, i.e. it scales "
"down the raw price rather than adding a surcharge to it."
),
},
)
# Computed fields
@computed_field # type: ignore[prop-decorator]
@property
def elecfee_consumption_amt_kwh(self) -> Optional[float]:
"""Electricity fee for consumed energy per kWh [amount/kWh].
This is the aggregation of fees that are to be paid by consumed energy per kWh - "
like network charge, concession fee, electricity charge."
Convenience attribute calculated from `elecfee_consumption_amt_wh`.
"""
if self.elecfee_consumption_amt_wh is None:
return None
return self.elecfee_consumption_amt_wh * 1000.0
@computed_field # type: ignore[prop-decorator]
@property
def elecfee_feedin_amt_kwh(self) -> Optional[float]:
"""Electricity fee for feed-in energy per kWh [amount/kWh].
This is the aggregation of fees that are to be paid by feed-in energy per kWh - "
like network charge, concession fee, electricity charge."
Convenience attribute calculated from `elecfee_feedin_amt_wh`.
"""
if self.elecfee_feedin_amt_wh is None:
return None
return self.elecfee_feedin_amt_wh * 1000.0
class ElecFeeProvider(PredictionProvider[ElecFeeDataRecord]):
"""Abstract base class for electricity fee providers.
Electricity fee providers predict fees on consumed and feed-in electricity to be used by
electricity and feed-in price providers.
ElecFeeProvider is a thread-safe singleton, ensuring only one instance of this class is created.
"""
# overload
records: List[ElecFeeDataRecord] = Field(
default_factory=list,
json_schema_extra={"description": "List of ElecFeeDataRecord records"},
)
@classmethod
@abstractmethod
def provider_id(cls) -> str:
return "ElecFeeProvider"
def enabled(self) -> bool:
return self.provider_id() == self.config.elecfee.provider