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())