"""Steve OCPP 서버 REST API 클라이언트 Steve 대시보드 API를 통해 충전기 원격 제어 수행. - RemoteStartTransaction: 결제 승인 후 충전 시작 - RemoteStopTransaction: 충전 원격 종료 """ import logging from typing import Optional import httpx from app.config import get_settings logger = logging.getLogger(__name__) settings = get_settings() class SteveClient: """Steve OCPP 서버와 HTTP 통신""" def __init__(self): self.base_url = settings.STEVE_BASE_URL.rstrip("/") self.auth = (settings.STEVE_API_USER, settings.STEVE_API_PASSWORD) async def _request( self, method: str, path: str, **kwargs ) -> Optional[dict]: url = f"{self.base_url}{path}" try: async with httpx.AsyncClient(verify=True, timeout=10.0) as client: resp = await client.request( method, url, auth=self.auth, **kwargs ) resp.raise_for_status() if resp.headers.get("content-type", "").startswith("application/json"): return resp.json() return {"status": "ok", "code": resp.status_code} except httpx.HTTPStatusError as e: logger.error(f"Steve API HTTP 에러: {e.response.status_code} {url}") return None except Exception as e: logger.error(f"Steve API 연결 실패: {e}") return None # ── 충전기 관리 ── async def get_charge_points(self) -> Optional[list]: """등록된 충전기 목록 조회""" return await self._request("GET", "/api/v1/chargepoints") async def get_charge_point(self, charge_box_id: str) -> Optional[dict]: """특정 충전기 상세 정보""" return await self._request("GET", f"/api/v1/chargepoints/{charge_box_id}") # ── 원격 제어 (핵심) ── async def remote_start_transaction( self, charge_box_id: str, id_tag: str, connector_id: int = 1, ) -> Optional[dict]: """충전기에 원격 충전 시작 명령 전송 결제 승인 완료 후 호출. Steve → 충전기 WebSocket으로 RemoteStartTransaction 전송. """ payload = { "chargeBoxId": charge_box_id, "connectorId": connector_id, "idTag": id_tag, } logger.info(f"RemoteStart 요청: {charge_box_id} tag={id_tag}") result = await self._request( "POST", "/api/v1/operations/RemoteStartTransaction", json=payload, ) if result: logger.info(f"RemoteStart 응답: {result}") return result async def remote_stop_transaction( self, charge_box_id: str, transaction_id: int, ) -> Optional[dict]: """충전기에 원격 충전 종료 명령 전송""" payload = { "chargeBoxId": charge_box_id, "transactionId": transaction_id, } logger.info(f"RemoteStop 요청: {charge_box_id} txn={transaction_id}") result = await self._request( "POST", "/api/v1/operations/RemoteStopTransaction", json=payload, ) return result # ── 트랜잭션 조회 ── async def get_transactions( self, charge_box_id: Optional[str] = None ) -> Optional[list]: """트랜잭션(충전 기록) 조회""" params = {} if charge_box_id: params["chargeBoxId"] = charge_box_id return await self._request( "GET", "/api/v1/transactions", params=params ) async def get_transaction(self, transaction_id: int) -> Optional[dict]: """특정 트랜잭션 상세""" return await self._request( "GET", f"/api/v1/transactions/{transaction_id}" ) # ── ID 태그 관리 ── async def add_id_tag(self, id_tag: str) -> Optional[dict]: """OCPP 인증용 ID 태그 등록""" payload = {"idTag": id_tag} return await self._request("POST", "/api/v1/idtags", json=payload) async def get_id_tags(self) -> Optional[list]: """등록된 ID 태그 목록""" return await self._request("GET", "/api/v1/idtags") # 싱글턴 steve_client = SteveClient()