Files
solar-pv-dashboard/backend/config.py
Mateusz Gruszczyński 138059945e poprawki i zmiany ux
2026-03-26 09:30:39 +01:00

290 lines
9.3 KiB
Python

from __future__ import annotations
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
PROJECT_DIR = BASE_DIR.parent
DATA_DIR = PROJECT_DIR / "data"
DATA_DIR.mkdir(exist_ok=True)
def _load_dotenv(path: Path) -> None:
if not path.exists():
return
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
if not key or key in os.environ:
continue
cleaned = value.strip().strip('"').strip("'")
os.environ[key] = cleaned
_load_dotenv(PROJECT_DIR / ".env")
def env_bool(name: str, default: bool = False) -> bool:
return os.getenv(name, str(default)).strip().lower() in {"1", "true", "yes", "on"}
def env_int(name: str, default: int) -> int:
try:
return int(os.getenv(name, str(default)))
except (TypeError, ValueError):
return default
def env_float(name: str, default: float) -> float:
try:
return float(os.getenv(name, str(default)).replace(",", "."))
except (TypeError, ValueError):
return default
APP_CONFIG = {
"name": os.getenv("APP_NAME", "PV Insight"),
"version": os.getenv("APP_VERSION", "1.3.0"),
"debug": env_bool("APP_DEBUG", False),
"api_prefix": "/api/v1",
"timezone": os.getenv("APP_TIMEZONE", "Europe/Warsaw"),
"host": os.getenv("APP_HOST", "0.0.0.0"),
"port": env_int("APP_PORT", 8105),
}
SITE_CONFIG = {
"site_name": os.getenv("SITE_NAME", "Home PV installation"),
"timezone": APP_CONFIG["timezone"],
"installed_power_kwp": env_float("PV_INSTALLED_POWER_KWP", 9.99),
"currency": os.getenv("SITE_CURRENCY", "PLN"),
"co2_factor_kg_per_kwh": env_float("CO2_FACTOR_KG_PER_KWH", 0.72),
}
INFLUXDB_CONFIG = {
"scheme": os.getenv("INFLUXDB_SCHEME", "http"),
"host": os.getenv("INFLUXDB_HOST", "127.0.0.1"),
"port": env_int("INFLUXDB_PORT", 8086),
"database": os.getenv("INFLUXDB_DATABASE", "ha"),
"username": os.getenv("INFLUXDB_USER", ""),
"password": os.getenv("INFLUXDB_PASSWORD", ""),
"verify_ssl": env_bool("INFLUXDB_VERIFY_SSL", False),
"timeout_seconds": env_int("INFLUXDB_TIMEOUT_SECONDS", 15),
}
STORAGE_CONFIG = {
"sqlite_path": os.getenv("APP_SQLITE_PATH", str(DATA_DIR / "pv_insight.sqlite3")),
}
CORS_ORIGINS = [
value.strip()
for value in os.getenv(
"CORS_ORIGINS",
"http://localhost:5173,http://127.0.0.1:5173,http://localhost:4173,http://127.0.0.1:4173",
).split(",")
if value.strip()
]
TIME_RANGES = {
"today": {"label": "Today", "special": "today"},
"yesterday": {"label": "Yesterday", "special": "yesterday"},
"6h": {"label": "6h", "seconds": 6 * 3600},
"12h": {"label": "12h", "seconds": 12 * 3600},
"24h": {"label": "24h", "seconds": 24 * 3600},
"48h": {"label": "48h", "seconds": 48 * 3600},
"1d": {"label": "1 day", "seconds": 1 * 24 * 3600},
"3d": {"label": "3 days", "seconds": 3 * 24 * 3600},
"7d": {"label": "7 days", "seconds": 7 * 24 * 3600},
"14d": {"label": "14 days", "seconds": 14 * 24 * 3600},
"30d": {"label": "30 days", "seconds": 30 * 24 * 3600},
"60d": {"label": "60 days", "seconds": 60 * 24 * 3600},
"90d": {"label": "90 days", "seconds": 90 * 24 * 3600},
"365d": {"label": "365 days", "seconds": 365 * 24 * 3600},
"ytd": {"label": "YTD", "special": "ytd"},
}
REALTIME = {
"refresh_seconds": env_int("REALTIME_REFRESH_SECONDS", 8),
"history_default_range": os.getenv("REALTIME_HISTORY_DEFAULT_RANGE", "today"),
}
ANALYTICS = {
"production_metric_id": "energy_total",
"fallback_power_metric_id": "ac_power",
"default_range": os.getenv("ANALYTICS_DEFAULT_RANGE", "30d"),
"default_bucket": os.getenv("ANALYTICS_DEFAULT_BUCKET", "day"),
"bucket_labels": {
"day": "Day",
"week": "Week",
"month": "Month",
"year": "Year",
},
"compare_modes": {
"none": "Comparison",
"previous_period": "Previous period",
"previous_year": "Previous year",
"previous_year_2": "2 years back",
"previous_year_3": "3 years back",
"previous_month_12": "12 months back",
"previous_month_24": "24 months back",
"custom_multi": "Custom ranges",
},
}
HISTORY = {
"enabled": env_bool("HISTORY_ENABLED", True),
"chunk_days": env_int("HISTORY_CHUNK_DAYS", 7),
"default_chunk_days": env_int("HISTORY_DEFAULT_CHUNK_DAYS", 7),
"auto_sync_enabled": env_bool("HISTORY_AUTO_SYNC_ENABLED", True),
"auto_sync_on_start": env_bool("HISTORY_AUTO_SYNC_ON_START", False),
"auto_sync_interval_minutes": env_int("HISTORY_AUTO_SYNC_INTERVAL_MINUTES", 30),
"include_today_in_sync": env_bool("HISTORY_INCLUDE_TODAY_IN_SYNC", False),
"bootstrap_start_date": os.getenv("HISTORY_BOOTSTRAP_START_DATE", "").strip(),
}
AUTH_CONFIG = {
"enabled": env_bool("AUTH_ENABLED", True),
"username": os.getenv("AUTH_USERNAME", "admin"),
"password": os.getenv("AUTH_PASSWORD", "change-me"),
"password_hash": os.getenv("AUTH_PASSWORD_HASH", "").strip(),
"display_name": os.getenv("AUTH_DISPLAY_NAME", "Operator"),
"secret_key": os.getenv("APP_SECRET_KEY", "pv-insight-dev-secret-change-me"),
"session_cookie_name": os.getenv("APP_SESSION_COOKIE_NAME", "pv_insight_session"),
"session_max_age_seconds": env_int("AUTH_SESSION_MAX_AGE_SECONDS", 60 * 60 * 12),
"cookie_secure": env_bool("AUTH_COOKIE_SECURE", False),
"cookie_samesite": os.getenv("AUTH_COOKIE_SAMESITE", "Lax"),
}
I18N = {
"default_language": os.getenv("APP_DEFAULT_LANGUAGE", "pl"),
"supported_languages": ["pl", "en"],
}
FRONTEND_DEFAULTS = {
"tab": os.getenv("FRONTEND_DEFAULT_TAB", "realtime"),
"theme": os.getenv("FRONTEND_THEME", "dark"),
"language": os.getenv("FRONTEND_LANGUAGE", I18N["default_language"]),
}
METRICS: dict[str, dict] = {}
STRINGS: list[dict] = []
def register_metric(
metric_id: str,
*,
entity_id: str,
measurement: str,
unit: str,
label: str,
kind: str = "gauge",
precision: int = 2,
) -> str | None:
entity_id = (entity_id or "").strip()
measurement = (measurement or "").strip()
if not entity_id or not measurement:
return None
METRICS[metric_id] = {
"entity_id": entity_id,
"measurement": measurement,
"unit": unit,
"label": label,
"kind": kind,
"precision": precision,
"enabled": True,
}
return metric_id
register_metric(
"ac_power",
entity_id=os.getenv("PV_AC_POWER_ENTITY", "sofarsolar_ac_power"),
measurement=os.getenv("PV_AC_POWER_MEASUREMENT", "W"),
unit="W",
label="AC Power",
precision=0,
)
register_metric(
"energy_total",
entity_id=os.getenv("PV_TOTAL_ENERGY_ENTITY", "sofarsolar_energy_total"),
measurement=os.getenv("PV_TOTAL_ENERGY_MEASUREMENT", "kWh"),
unit="kWh",
label="Total Energy",
kind="counter",
precision=2,
)
register_metric(
"inverter_temp",
entity_id=os.getenv("PV_INVERTER_TEMP_ENTITY", "sofarsolar_temprature_inverter"),
measurement=os.getenv("PV_INVERTER_TEMP_MEASUREMENT", "°C"),
unit="°C",
label="Inverter Temperature",
precision=1,
)
STRING_DEFAULTS = {
1: {"label": "DC1", "power": "sofarsolar_dc1_power", "voltage": "sofarsolar_dc1_voltage"},
2: {"label": "DC2", "power": "sofarsolar_dc2_power", "voltage": "sofarsolar_dc2_voltage"},
3: {"label": "DC3", "power": "", "voltage": ""},
4: {"label": "DC4", "power": "", "voltage": ""},
}
for index, defaults in STRING_DEFAULTS.items():
label = os.getenv(f"PV_STRING_{index}_LABEL", defaults["label"]).strip() or defaults["label"]
power_metric_id = register_metric(
f"string_{index}_power",
entity_id=os.getenv(f"PV_STRING_{index}_POWER_ENTITY", defaults["power"]),
measurement=os.getenv(f"PV_STRING_{index}_POWER_MEASUREMENT", "W"),
unit="W",
label=f"{label} power",
precision=0,
)
voltage_metric_id = register_metric(
f"string_{index}_voltage",
entity_id=os.getenv(f"PV_STRING_{index}_VOLTAGE_ENTITY", defaults["voltage"]),
measurement=os.getenv(f"PV_STRING_{index}_VOLTAGE_MEASUREMENT", "V"),
unit="V",
label=f"{label} voltage",
precision=1,
)
if power_metric_id or voltage_metric_id:
STRINGS.append(
{
"id": f"string_{index}",
"label": label,
"metrics": {
key: value
for key, value in {
"power": power_metric_id,
"voltage": voltage_metric_id,
}.items()
if value
},
}
)
MODULES = {
"realtime_overview": True,
"realtime_history": True,
"analytics": "energy_total" in METRICS or "ac_power" in METRICS,
"comparison": "energy_total" in METRICS or "ac_power" in METRICS,
"distribution_pie": "energy_total" in METRICS or "ac_power" in METRICS,
"strings": len(STRINGS) > 0,
"temperatures": "inverter_temp" in METRICS,
"historical_import": HISTORY["enabled"],
"phases": False,
"faults": False,
"settings_panel": True,
}
STATUS_METRICS = [metric_id for metric_id in ["inverter_temp"] if metric_id in METRICS]
VISIBLE_ENTITY_TABLE = list(METRICS.keys())