power-compare/app/api/isolarcloud.py
dlawler489 84c4922bbc Initial commit: solar usage vs AER plan comparison app
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>
2026-09-09 11:07:01 +10:00

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]})