Source code for models.port
"""Port model for the electric recreational port simulator."""
import json
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional
if TYPE_CHECKING:
from .bess import BESS
from .boat import Boat
from .charger import Charger
from .pv import PV
[docs]
@dataclass
class Port:
"""
Electric recreational port with boats, chargers, PV, BESS, and optional tariff.
Attributes:
name: Port name.
contracted_power: Contracted power limit (kW).
lat: Latitude.
lon: Longitude.
boats: List of boats.
chargers: List of chargers.
pv_systems: List of PV systems.
bess_systems: List of BESS systems.
tariff_path: Optional path to tariff JSON; if None, default_tariff.json is tried.
"""
name: str
contracted_power: int
lat: float
lon: float
boats: List["Boat"] = field(default_factory=list)
chargers: List["Charger"] = field(default_factory=list)
pv_systems: List["PV"] = field(default_factory=list)
bess_systems: List["BESS"] = field(default_factory=list)
tariff_path: Optional[str] = None
_tariff_data: Optional[Dict] = field(default=None, init=False, repr=False)
def __post_init__(self):
"""Validate attributes and load tariff from tariff_path or default path if present."""
if self.contracted_power <= 0:
raise ValueError("Contracted power must be positive")
if not -90 <= self.lat <= 90:
raise ValueError("Latitude must be between -90 and 90")
if not -180 <= self.lon <= 180:
raise ValueError("Longitude must be between -180 and 180")
if self.tariff_path:
self._load_tariff()
else:
default_path = (
Path(__file__).parent.parent
/ "assets"
/ "tariff"
/ "default_tariff.json"
)
if default_path.exists():
self.tariff_path = str(default_path)
self._load_tariff()
def _load_tariff(self) -> None:
"""Load tariff JSON from tariff_path into _tariff_data."""
if not self.tariff_path:
return
tariff_file = Path(self.tariff_path)
if not tariff_file.exists():
raise FileNotFoundError(f"Tariff file not found: {self.tariff_path}")
try:
with open(tariff_file, "r", encoding="utf-8") as f:
self._tariff_data = json.load(f)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in tariff file: {tariff_file}") from e
@property
def tariff(self) -> Optional[Dict]:
"""Loaded tariff data (from tariff_path); None if no tariff loaded."""
return self._tariff_data
[docs]
def get_tariff_price(self, timestamp: datetime) -> float:
"""
Return electricity price per kWh for the given timestamp (15-minute resolution).
Args:
timestamp: Time to look up.
Returns:
Price per kWh, or 0.0 if no tariff or slot not found.
"""
if not self._tariff_data or "tariff" not in self._tariff_data:
return 0.0
weekday = timestamp.weekday()
day_key = str(weekday)
if day_key not in self._tariff_data["tariff"]:
return 0.0
day_pricing = self._tariff_data["tariff"][day_key]["pricing"]
minute = timestamp.minute
rounded_minute = (minute // 15) * 15
time_str = timestamp.strftime(f"%H:{rounded_minute:02d}")
return day_pricing.get(time_str, 0.0)
[docs]
def add_boat(self, boat: "Boat") -> None:
"""Append a boat to the port's boat list."""
self.boats.append(boat)
[docs]
def add_charger(self, charger: "Charger") -> None:
"""Append a charger to the port's charger list."""
self.chargers.append(charger)
[docs]
def add_pv(self, pv: "PV") -> None:
"""Append a PV system to the port's pv_systems list."""
self.pv_systems.append(pv)
[docs]
def add_bess(self, bess: "BESS") -> None:
"""Append a BESS to the port's bess_systems list."""
self.bess_systems.append(bess)
def __repr__(self) -> str:
return (
f"Port(name='{self.name}', contracted_power={self.contracted_power}kW, "
f"coordinates=({self.lat}, {self.lon}), "
f"boats={len(self.boats)}, chargers={len(self.chargers)}, "
f"pv={len(self.pv_systems)}, bess={len(self.bess_systems)})"
)