FastAPI app that pulls interval usage from Sungrow iSolarCloud (OAuth2), caches AER Consumer Data Right electricity plan data, and compares actual metered usage against every available plan to estimate real annual cost (TOU, tiered feed-in, and flat tariffs). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
81 lines
3 KiB
Python
81 lines
3 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel
|
|
|
|
from app.config import settings
|
|
from app.db.repositories import get_sync_state
|
|
from app.db.session import get_session
|
|
|
|
router = APIRouter(prefix="/api/isolarcloud", tags=["isolarcloud"])
|
|
|
|
|
|
@router.get("/status")
|
|
def status() -> dict:
|
|
token_key = "isolarcloud_oauth_access_token" if settings.isolarcloud_auth_mode == "oauth2" else "isolarcloud_token"
|
|
with get_session() as session:
|
|
has_token = bool(get_sync_state(session, token_key))
|
|
return {"use_mock": settings.use_mock_isolarcloud, "auth_mode": settings.isolarcloud_auth_mode, "connected": has_token}
|
|
|
|
|
|
@router.get("/oauth/authorize-url")
|
|
def oauth_authorize_url() -> dict:
|
|
from app.isolarcloud.oauth_client import ISolarCloudOAuthClient
|
|
|
|
return {"url": ISolarCloudOAuthClient().authorize_url()}
|
|
|
|
|
|
class OAuthCodeIn(BaseModel):
|
|
code: str
|
|
|
|
|
|
@router.post("/oauth/exchange")
|
|
def oauth_exchange(body: OAuthCodeIn) -> JSONResponse:
|
|
"""Completes the OAuth2 consent flow: trades the `code` param (copied
|
|
from the address bar after the user approved access) for a token pair,
|
|
and returns the plants the user actually authorized."""
|
|
from app.isolarcloud.oauth_client import ISolarCloudAuthError, ISolarCloudOAuthClient
|
|
|
|
client = ISolarCloudOAuthClient()
|
|
try:
|
|
auth_ps_list = client.exchange_code(body.code)
|
|
except ISolarCloudAuthError as e:
|
|
return JSONResponse({"ok": False, "error": str(e)}, status_code=400)
|
|
except Exception as e:
|
|
return JSONResponse({"ok": False, "error": f"Unexpected error: {e}"}, status_code=502)
|
|
|
|
plants = []
|
|
try:
|
|
plants = client.get_plant_list()
|
|
except Exception:
|
|
pass
|
|
plants_by_id = {str(p.get("ps_id")): p.get("ps_name") for p in plants}
|
|
return JSONResponse(
|
|
{
|
|
"ok": True,
|
|
"authorized_plants": [{"ps_id": ps_id, "ps_name": plants_by_id.get(ps_id)} for ps_id in auth_ps_list],
|
|
}
|
|
)
|
|
|
|
|
|
@router.post("/connect")
|
|
def connect() -> JSONResponse:
|
|
"""Logs in with the credentials configured in .env and, on success,
|
|
returns the account's visible plants so the user can pick a
|
|
`isolarcloud_plant_id` in Settings if they have more than one."""
|
|
from app.isolarcloud.client import ISolarCloudAuthError, ISolarCloudClient
|
|
|
|
if settings.use_mock_isolarcloud:
|
|
return JSONResponse({"ok": False, "error": "USE_MOCK_ISOLARCLOUD is still true -- set it to false in .env first."}, status_code=400)
|
|
|
|
client = ISolarCloudClient()
|
|
try:
|
|
client.login()
|
|
plants = client.get_plant_list()
|
|
except ISolarCloudAuthError as e:
|
|
return JSONResponse({"ok": False, "error": str(e)}, status_code=400)
|
|
except Exception as e: # network/HTTP errors etc.
|
|
return JSONResponse({"ok": False, "error": f"Unexpected error: {e}"}, status_code=502)
|
|
|
|
return JSONResponse({"ok": True, "plants": [{"ps_id": p.get("ps_id"), "ps_name": p.get("ps_name")} for p in plants]})
|