Initial commit: Check-My-IRVE SaaS (OCPP 1.6, Audit, Diagnostics)
This commit is contained in:
commit
192be86487
44 changed files with 2070 additions and 0 deletions
179
backend/app/main.py
Normal file
179
backend/app/main.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse
|
||||
from app.ocpp.router import CustomChargePoint
|
||||
from app.core.session_manager import session_mgr
|
||||
from app.core.diagnostics import analyze_frames, check_electrical
|
||||
from ocpp.v16 import call as v16_call
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
app = FastAPI(title="Check-My-IRVE API", version="3.5.0")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
class SocketAdapter:
|
||||
def __init__(self, websocket: WebSocket, cp_id: str):
|
||||
self._ws = websocket
|
||||
self.cp_id = cp_id
|
||||
|
||||
async def recv(self):
|
||||
msg = await self._ws.receive_text()
|
||||
try:
|
||||
parsed = json.loads(msg)
|
||||
msg_type = parsed[0]
|
||||
unique_id = parsed[1]
|
||||
if msg_type == 2: action, payload = parsed[2], parsed[3] if len(parsed) > 3 else {}
|
||||
elif msg_type == 3: action, payload = None, parsed[2] if len(parsed) > 2 else {}
|
||||
elif msg_type == 4: action, payload = None, {"errorCode": parsed[2]}
|
||||
else: return msg
|
||||
await session_mgr.log_frame(self.cp_id, "IN", msg_type, action, unique_id, payload)
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur parsing IN: {e}")
|
||||
return msg
|
||||
|
||||
async def send(self, message):
|
||||
try:
|
||||
parsed = json.loads(message)
|
||||
msg_type = parsed[0]
|
||||
unique_id = parsed[1]
|
||||
if msg_type == 2: action, payload = parsed[2], parsed[3] if len(parsed) > 3 else {}
|
||||
elif msg_type == 3: action, payload = None, parsed[2] if len(parsed) > 2 else {}
|
||||
elif msg_type == 4: action, payload = None, {"errorCode": parsed[2]}
|
||||
else: return await self._ws.send_text(message)
|
||||
await session_mgr.log_frame(self.cp_id, "OUT", msg_type, action, unique_id, payload)
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur parsing OUT: {e}")
|
||||
await self._ws.send_text(message)
|
||||
|
||||
active_charge_points = {}
|
||||
|
||||
@app.websocket("/ocpp/{client_id}")
|
||||
async def websocket_endpoint(websocket: WebSocket, client_id: str):
|
||||
subprotocol = websocket.headers.get("sec-websocket-protocol", "Unknown")
|
||||
await websocket.accept(subprotocol="ocpp1.6")
|
||||
adapter = SocketAdapter(websocket, client_id)
|
||||
cp = CustomChargePoint(client_id, adapter)
|
||||
cp.subprotocol = subprotocol
|
||||
active_charge_points[client_id] = cp
|
||||
|
||||
if session_mgr.redis:
|
||||
data = await session_mgr.redis.hget("chargepoints", client_id)
|
||||
if data:
|
||||
cp_data = json.loads(data)
|
||||
cp_data["reconnections"] = cp_data.get("reconnections", 0) + 1
|
||||
await session_mgr.redis.hset("chargepoints", client_id, json.dumps(cp_data))
|
||||
|
||||
try:
|
||||
await cp.start()
|
||||
except WebSocketDisconnect as e:
|
||||
logging.info(f"Borne {client_id} déconnectée (Code: {e.code}).")
|
||||
await session_mgr.update_disconnect(client_id, e.code)
|
||||
await session_mgr.update_cp_status(client_id, "Disconnected")
|
||||
if client_id in active_charge_points: del active_charge_points[client_id]
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur OCPP sur {client_id}: {e}")
|
||||
|
||||
@app.get("/api/chargepoints")
|
||||
async def get_cps(): return await session_mgr.get_cps()
|
||||
|
||||
@app.get("/api/chargepoints/{cp_id}/frames")
|
||||
async def get_frames(cp_id: str): return await session_mgr.get_frames(cp_id)
|
||||
|
||||
@app.delete("/api/chargepoints/{cp_id}/frames")
|
||||
async def purge_frames(cp_id: str):
|
||||
await session_mgr.purge_frames(cp_id)
|
||||
return {"status": "success"}
|
||||
|
||||
@app.post("/api/chargepoints/{cp_id}/diagnostics")
|
||||
async def get_diagnostics(cp_id: str, payload: dict):
|
||||
frames = await session_mgr.get_frames(cp_id)
|
||||
cps = await session_mgr.get_cps()
|
||||
cp_data = next((cp for cp in cps if cp["id"] == cp_id), {})
|
||||
installer_config = payload.get("installer_config", {})
|
||||
return analyze_frames(frames, cp_data, installer_config)
|
||||
|
||||
@app.get("/api/chargepoints/{cp_id}/report", response_class=HTMLResponse)
|
||||
async def get_report(cp_id: str):
|
||||
frames = await session_mgr.get_frames(cp_id)
|
||||
cps = await session_mgr.get_cps()
|
||||
cp_data = next((cp for cp in cps if cp["id"] == cp_id), {})
|
||||
findings = analyze_frames(frames, cp_data, {})
|
||||
|
||||
html = f"""
|
||||
<html><head><title>Rapport - {cp_id}</title>
|
||||
<style>body{{font-family:sans-serif;margin:40px}}pre{{background:#0f172a;color:#e2e8f0;padding:15px}}</style></head>
|
||||
<body><h1>Rapport IRVE</h1><p>Borne: {cp_id}</p>
|
||||
<h2>Anomalies</h2>{''.join([f'<div style="border-left:4px solid red;padding:10px;margin-bottom:10px"><b>{f["title"]}</b><br>{f["action"]}</div>' for f in findings])}
|
||||
<h2>Trames</h2><pre>{json.dumps(frames[:20], indent=2)}</pre>
|
||||
</body></html>
|
||||
"""
|
||||
return HTMLResponse(content=html)
|
||||
|
||||
@app.post("/api/chargepoints/{cp_id}/audit")
|
||||
async def audit_config(payload: dict):
|
||||
borne_config_dict = {k["key"]: k for k in payload.get("borne_config", {}).get("configurationKey", [])}
|
||||
installer_config = payload.get("installer_config", {})
|
||||
|
||||
audit_result = []
|
||||
|
||||
REFERENCE_CONFIG = {
|
||||
"HeartbeatInterval": {"expected": "300", "severity": "warning", "readonly": False},
|
||||
"WebSocketPingInterval": {"expected": "60", "severity": "critical", "readonly": False},
|
||||
"MeterValueSampleInterval": {"expected": "10", "severity": "info", "readonly": False},
|
||||
"ConnectionTimeOut": {"expected": "60", "severity": "warning", "readonly": False}
|
||||
}
|
||||
for key, ref in REFERENCE_CONFIG.items():
|
||||
borne_val = borne_config_dict.get(key, {}).get("value", "Manquant")
|
||||
audit_result.append({
|
||||
"key": key, "borne_val": borne_val, "expected_val": ref["expected"],
|
||||
"match": borne_val == ref["expected"], "severity": ref["severity"], "readonly": ref["readonly"]
|
||||
})
|
||||
|
||||
elec_findings = check_electrical(borne_config_dict, installer_config)
|
||||
for f in elec_findings:
|
||||
audit_result.append({
|
||||
"key": f['title'], "borne_val": f['explication'], "expected_val": f['action'],
|
||||
"match": False, "severity": f['severity'], "readonly": True
|
||||
})
|
||||
|
||||
return audit_result
|
||||
|
||||
@app.post("/api/chargepoints/{cp_id}/command")
|
||||
async def send_command(cp_id: str, command: dict):
|
||||
if cp_id not in active_charge_points: return {"error": "CP not connected"}
|
||||
cp = active_charge_points[cp_id]
|
||||
action = command.get("action")
|
||||
payload = command.get("payload", {})
|
||||
try:
|
||||
if action == "TriggerMessage": req = v16_call.TriggerMessagePayload(requested_message=payload.get("requested_message", "StatusNotification"), connector_id=payload.get("connector_id", 0))
|
||||
elif action == "GetConfiguration": req = v16_call.GetConfigurationPayload(key=payload.get("key", []))
|
||||
elif action == "RemoteStartTransaction": req = v16_call.RemoteStartTransactionPayload(id_tag="CHARGEMAP_PASS", connector_id=1)
|
||||
elif action == "Reset": req = v16_call.ResetPayload(type="Soft")
|
||||
else: return {"error": "Unsupported"}
|
||||
resp = await cp.call(req)
|
||||
return {"status": "success", "response": resp}
|
||||
except Exception as e: return {"error": str(e)}
|
||||
|
||||
@app.websocket("/api/live")
|
||||
async def websocket_live(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
if not session_mgr.redis:
|
||||
await websocket.send_text(json.dumps({"error": "Redis indisponible"}))
|
||||
await websocket.close()
|
||||
return
|
||||
pubsub = session_mgr.redis.pubsub()
|
||||
await pubsub.subscribe("live_frames")
|
||||
try:
|
||||
async for message in pubsub.listen():
|
||||
if message['type'] == 'message': await websocket.send_text(message['data'].decode('utf-8'))
|
||||
except: pass
|
||||
finally: await pubsub.unsubscribe("live_frames")
|
||||
Loading…
Add table
Add a link
Reference in a new issue