diff --git a/scripts/analysis/battery_sizing_screen.py b/scripts/analysis/battery_sizing_screen.py index 23eb50d..8910bb1 100644 --- a/scripts/analysis/battery_sizing_screen.py +++ b/scripts/analysis/battery_sizing_screen.py @@ -5,6 +5,14 @@ Ekonomický screening velikosti baterie (15min, jednobusová energie). Typicky BA81: fixní nákup + prodej spot, limity výkonu z baterie min(0,5C, střídač), export/import podle připojení. Načte OTE z Postgres (stejná DB jako EMS) nebo z CSV. +Připojení k DB (deploy / Docker): + - Postgres v compose poslouchá na ``EMS_DB_BIND:5432`` (výchozí 127.0.0.1). ``connection refused`` + = služba ``db`` neběží, nebo je port vázaný jen na jinou IP (WireGuard) → nastav stejný host. + - Skript načte první nalezené ``.env`` z ``…/ems-deploy/.env`` nebo ``…/app/.env`` (není-li + ``--no-auto-env``) a doplní ``PGUSER``/``PGPASSWORD`` z ``DB_USER``/``DB_PASSWORD``. + - Nebo ``DATABASE_URL`` / ``postgresql://USER:PASS@HOST:5432/ems`` (na hostu HOST=127.0.0.1 + nebo EMS_DB_BIND, ne ``db`` — to je jen uvnitř Docker sítě). + Příklad: python3 scripts/analysis/battery_sizing_screen.py \\ --db \\ @@ -31,6 +39,7 @@ import os import sys from dataclasses import dataclass from datetime import date, datetime, timedelta +from pathlib import Path from typing import Iterable, Sequence try: @@ -93,6 +102,41 @@ def effective_sell_kc_kwh(raw_ote: float, margin_fixed: float, margin_pct: float return raw_ote + margin_fixed + (raw_ote * margin_pct / 100.0) +def load_env_file(path: Path) -> None: + if not path.is_file(): + return + for line in path.read_text(encoding="utf-8", errors="replace").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, _, v = line.partition("=") + k, v = k.strip(), v.strip().strip('"').strip("'") + if not k: + continue + if k not in os.environ or os.environ.get(k, "") == "": + os.environ[k] = v + + +def apply_auto_env_files() -> None: + """Na produkci: /opt/ems-deploy/.env (když tam je docker-compose), pak app/.env nebo kořen repa.""" + script = Path(__file__).resolve() + if len(script.parents) >= 3: + deploy_root = script.parents[2] + if (deploy_root / "docker-compose.yml").is_file(): + load_env_file(deploy_root / ".env") + if len(script.parents) >= 2: + load_env_file(script.parents[1] / ".env") + + +def sync_pg_env_from_db_vars() -> None: + if not os.environ.get("PGUSER") and os.environ.get("DB_USER"): + os.environ["PGUSER"] = os.environ["DB_USER"] + if not os.environ.get("PGPASSWORD") and os.environ.get("DB_PASSWORD"): + os.environ["PGPASSWORD"] = os.environ["DB_PASSWORD"] + if not os.environ.get("PGDATABASE"): + os.environ["PGDATABASE"] = "ems" + + def load_prices_csv(path: str) -> list[tuple[datetime, float]]: out: list[tuple[datetime, float]] = [] with open(path, newline="", encoding="utf-8") as f: @@ -118,13 +162,23 @@ def load_prices_db(date_from: date, date_to: date) -> list[tuple[datetime, float t0 = datetime.combine(date_from, datetime.min.time(), tzinfo=prg).astimezone(timezone.utc) t1 = datetime.combine(date_to, datetime.min.time(), tzinfo=prg).astimezone(timezone.utc) - conn = psycopg2.connect( - host=os.environ.get("PGHOST", "127.0.0.1"), - port=int(os.environ.get("PGPORT", "5432")), - dbname=os.environ.get("PGDATABASE", "ems"), - user=os.environ.get("PGUSER", os.environ.get("DB_USER", "ems_user")), - password=os.environ.get("PGPASSWORD", os.environ.get("DB_PASSWORD", "")), + dsn = ( + os.environ.get("DATABASE_URL") + or os.environ.get("EMS_DATABASE_URL") + or os.environ.get("POSTGRES_URL") ) + if dsn: + if dsn.startswith("postgres://"): + dsn = "postgresql://" + dsn[len("postgres://") :] + conn = psycopg2.connect(dsn) + else: + conn = psycopg2.connect( + host=os.environ.get("PGHOST", "127.0.0.1"), + port=int(os.environ.get("PGPORT", "5432")), + dbname=os.environ.get("PGDATABASE", "ems"), + user=os.environ.get("PGUSER", os.environ.get("DB_USER", "ems_user")), + password=os.environ.get("PGPASSWORD", os.environ.get("DB_PASSWORD", "")), + ) cur = conn.cursor() cur.execute( """ @@ -270,7 +324,19 @@ def simulate_year( def main() -> None: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - ap.add_argument("--db", action="store_true", help="Načti OTE z Postgres (env PG* / DB_*)") + ap.add_argument("--db", action="store_true", help="Načti OTE z Postgres (env PG* / DB_* / DATABASE_URL)") + ap.add_argument( + "--no-auto-env", + action="store_true", + help="Nenačítej automaticky .env z ems-deploy/ ani app/", + ) + ap.add_argument("--env-file", type=str, default="", help="Dodatečný soubor .env (po auto-env)") + ap.add_argument( + "--pg-host", + type=str, + default="", + help="Přepíše PGHOST (např. stejná IP jako EMS_DB_BIND ve compose)", + ) ap.add_argument("--price-csv", type=str, default="", help="CSV: interval_start, sell_raw_price_czk_kwh") ap.add_argument("--date-from", type=str, required=True) ap.add_argument("--date-to", type=str, required=True) @@ -291,6 +357,13 @@ def main() -> None: d0 = date.fromisoformat(args.date_from) d1 = date.fromisoformat(args.date_to) if args.db: + if not args.no_auto_env: + apply_auto_env_files() + if args.env_file: + load_env_file(Path(args.env_file)) + sync_pg_env_from_db_vars() + if args.pg_host: + os.environ["PGHOST"] = args.pg_host series = load_prices_db(d0, d1) elif args.price_csv: series = load_prices_csv(args.price_csv)