All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 24s
Sungrow's OAuth server keeps only one active token session per (app, account), not per plant -- with Kallangur and McDowall each managing their own independent token against the same account, whichever one refreshed last was silently invalidating the other's refresh token. Both instances now share one token file (a common bind-mounted volume) instead of each holding their own. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
87 lines
3.3 KiB
Python
87 lines
3.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
|
|
from app.isolarcloud import shared_token_store
|
|
|
|
router = APIRouter(prefix="/api/isolarcloud", tags=["isolarcloud"])
|
|
|
|
|
|
@router.get("/status")
|
|
def status() -> dict:
|
|
if settings.isolarcloud_auth_mode == "oauth2" and shared_token_store.enabled():
|
|
with shared_token_store.locked() as fd:
|
|
state = shared_token_store.read(fd)
|
|
has_token = bool(state and state.get("access_token"))
|
|
else:
|
|
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]})
|