290 lines
9.3 KiB
Python
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", "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", "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": "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())
|