This commit is contained in:
1 parent
646ba3be81
commit
aae5b73581
14 files changed
+883
-142
No files matched your search
+25
-12
@@ -1,6 +1,6 @@
|
||||
"""Configuration contains environment variable names, never upload credentials."""
|
||||
"""Private runtime configuration; credential fields are excluded from reprs."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
import math
|
||||
from pathlib import Path
|
||||
@@ -9,6 +9,8 @@ import tomllib
|
||||
from urllib.parse import urlsplit
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
LEGACY_HISTORY_SETTINGS = {"access_key_id_env", "access_key_value_env", "lookback_days"}
|
||||
|
||||
|
||||
def utc_text(value: datetime) -> str:
|
||||
if value.tzinfo is None:
|
||||
@@ -52,6 +54,7 @@ class UploadConfig:
|
||||
request_timeout_seconds: float = 10
|
||||
retry_initial_seconds: float = 5
|
||||
retry_max_seconds: float = 300
|
||||
token: str = field(default="", repr=False)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -67,12 +70,13 @@ class HttpConfig:
|
||||
class HistoryConfig:
|
||||
enabled: bool = False
|
||||
pv_system_id: str = ""
|
||||
access_key_id_env: str = "SOLARWEB_ACCESS_KEY_ID"
|
||||
access_key_value_env: str = "SOLARWEB_ACCESS_KEY_VALUE"
|
||||
lookback_days: int = 7
|
||||
username_env: str = "SOLARWEB_USERNAME"
|
||||
password_env: str = "SOLARWEB_PASSWORD"
|
||||
lag_seconds: int = 7200
|
||||
check_interval_seconds: int = 3600
|
||||
request_timeout_seconds: float = 10
|
||||
request_timeout_seconds: float = 45
|
||||
username: str = field(default="", repr=False)
|
||||
password: str = field(default="", repr=False)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -118,7 +122,8 @@ def load_config(path: Path) -> Config:
|
||||
"inverter": set(InverterConfig.__dataclass_fields__),
|
||||
"upload": set(UploadConfig.__dataclass_fields__),
|
||||
"http": set(HttpConfig.__dataclass_fields__),
|
||||
"history": set(HistoryConfig.__dataclass_fields__),
|
||||
# Keep existing private configurations valid while using the full retention window.
|
||||
"history": set(HistoryConfig.__dataclass_fields__) | LEGACY_HISTORY_SETTINGS,
|
||||
}.items():
|
||||
if not isinstance(data.get(section, {}), dict):
|
||||
raise ValueError(f"{section} must be a table")
|
||||
@@ -157,6 +162,10 @@ def load_config(path: Path) -> Config:
|
||||
raise ValueError("upload.url must be a string")
|
||||
if upload.url:
|
||||
_url(upload.url, "upload.url", upload=True)
|
||||
if not isinstance(upload.token, str):
|
||||
raise ValueError("upload.token must be a string")
|
||||
if upload.token and any(ord(c) < 33 or ord(c) > 126 for c in upload.token):
|
||||
raise ValueError("upload.token must be a bearer token without whitespace")
|
||||
if not isinstance(upload.token_env, str) or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", upload.token_env):
|
||||
raise ValueError("upload.token_env must be an environment variable name")
|
||||
if isinstance(upload.batch_size, bool) or not isinstance(upload.batch_size, int) or not 1 <= upload.batch_size <= 500:
|
||||
@@ -175,21 +184,25 @@ def load_config(path: Path) -> Config:
|
||||
raise ValueError("http.base_path must be empty or a URL path without a trailing slash")
|
||||
if isinstance(http.history_days, bool) or not isinstance(http.history_days, int) or not 1 <= http.history_days <= 90:
|
||||
raise ValueError("http.history_days must be an integer between 1 and 90")
|
||||
history = HistoryConfig(**data.get("history", {}))
|
||||
history = HistoryConfig(**{key: value for key, value in data.get("history", {}).items()
|
||||
if key not in LEGACY_HISTORY_SETTINGS})
|
||||
if not isinstance(history.enabled, bool):
|
||||
raise ValueError("history.enabled must be true or false")
|
||||
if not isinstance(history.pv_system_id, str) or (history.pv_system_id and not re.fullmatch(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}", history.pv_system_id)):
|
||||
raise ValueError("history.pv_system_id must be a UUID")
|
||||
if history.enabled and not history.pv_system_id:
|
||||
raise ValueError("Set history.pv_system_id before enabling Solar.web recovery")
|
||||
for name in ("access_key_id_env", "access_key_value_env"):
|
||||
for name in ("username", "password"):
|
||||
if not isinstance(getattr(history, name), str):
|
||||
raise ValueError(f"history.{name} must be a string")
|
||||
if (history.username or history.password) and (not history.username.strip() or not history.password):
|
||||
raise ValueError("Set both history.username and history.password, or leave both empty to use environment variables")
|
||||
for name in ("username_env", "password_env"):
|
||||
if not isinstance(getattr(history, name), str) or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", getattr(history, name)):
|
||||
raise ValueError(f"history.{name} must name an environment variable")
|
||||
for name, minimum in [("lookback_days", 1), ("lag_seconds", 3600), ("check_interval_seconds", 3600)]:
|
||||
for name, minimum in [("lag_seconds", 3600), ("check_interval_seconds", 3600)]:
|
||||
value = getattr(history, name)
|
||||
if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
|
||||
raise ValueError(f"history.{name} must be an integer of at least {minimum}")
|
||||
if history.lookback_days > 30 or (history.enabled and history.lookback_days > retention):
|
||||
raise ValueError("history.lookback_days must be at most 30 and no longer than retention_days when enabled")
|
||||
_number(history.request_timeout_seconds, "history.request_timeout_seconds", 0.1)
|
||||
return Config(collector, inverter, upload, http, history)
|
||||
Reference in new issue
Block a user