first commit
This commit is contained in:
289
backend/config.py
Normal file
289
backend/config.py
Normal file
@@ -0,0 +1,289 @@
|
||||
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", "Domowa instalacja PV"),
|
||||
"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": "Dzis", "special": "today"},
|
||||
"yesterday": {"label": "Wczoraj", "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 dzien", "seconds": 1 * 24 * 3600},
|
||||
"3d": {"label": "3 dni", "seconds": 3 * 24 * 3600},
|
||||
"7d": {"label": "7 dni", "seconds": 7 * 24 * 3600},
|
||||
"14d": {"label": "14 dni", "seconds": 14 * 24 * 3600},
|
||||
"30d": {"label": "30 dni", "seconds": 30 * 24 * 3600},
|
||||
"60d": {"label": "60 dni", "seconds": 60 * 24 * 3600},
|
||||
"90d": {"label": "90 dni", "seconds": 90 * 24 * 3600},
|
||||
"365d": {"label": "365 dni", "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", "6h"),
|
||||
}
|
||||
|
||||
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": "Dzien",
|
||||
"week": "Tydzien",
|
||||
"month": "Miesiac",
|
||||
"year": "Rok",
|
||||
},
|
||||
"compare_modes": {
|
||||
"none": "Porownanie",
|
||||
"previous_period": "Poprzedni okres",
|
||||
"previous_year": "Poprzedni rok",
|
||||
"previous_year_2": "2 lata wstecz",
|
||||
"previous_year_3": "3 lata wstecz",
|
||||
"previous_month_12": "12 miesiecy wstecz",
|
||||
"previous_month_24": "24 miesiace wstecz",
|
||||
"custom_multi": "Wlasne zakresy",
|
||||
},
|
||||
}
|
||||
|
||||
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="Moc AC",
|
||||
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="Energia laczna",
|
||||
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="Temperatura falownika",
|
||||
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} moc",
|
||||
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} napiecie",
|
||||
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())
|
||||
Reference in New Issue
Block a user