Swap to private solarweb access
Deploy Solar Relay / deploy (push) Successful in 56s

This commit is contained in:
franzz committed 2026-11-02 18:06:01 +01:00
1 parent 646ba3be81
commit aae5b73581
14 files changed
+883 -142

No files matched your search

+25 -12
View File
@@ -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)