156 lines
6.3 KiB
Python
156 lines
6.3 KiB
Python
import time
|
|
import json
|
|
import logging
|
|
from collections import defaultdict, deque
|
|
from datetime import datetime, timezone
|
|
|
|
logger = logging.getLogger("session_manager")
|
|
|
|
try:
|
|
import redis.asyncio as aioredis
|
|
REDIS_AVAILABLE = True
|
|
except ImportError:
|
|
REDIS_AVAILABLE = False
|
|
|
|
def normalize_keys(obj):
|
|
"""Convertit récursivement les clés snake_case du payload OCPP en camelCase."""
|
|
if isinstance(obj, dict):
|
|
new_dict = {}
|
|
for k, v in obj.items():
|
|
parts = k.split('_')
|
|
new_key = parts[0] + ''.join(p.title() for p in parts[1:])
|
|
new_dict[new_key] = normalize_keys(v)
|
|
return new_dict
|
|
elif isinstance(obj, list):
|
|
return [normalize_keys(v) for v in obj]
|
|
return obj
|
|
|
|
class SessionManager:
|
|
def __init__(self, redis_url=None):
|
|
self.redis = None
|
|
self.memory_cps = {}
|
|
self.memory_frames = defaultdict(lambda: deque(maxlen=200))
|
|
self.pending_calls = {}
|
|
self.action_map = {}
|
|
|
|
if redis_url and REDIS_AVAILABLE:
|
|
try:
|
|
self.redis = aioredis.from_url(redis_url)
|
|
logger.info("SessionManager connecté à Redis.")
|
|
except Exception as e:
|
|
logger.warning(f"Redis indisponible: {e}")
|
|
self.redis = None
|
|
|
|
async def register_cp(self, cp_id, model, vendor, subprotocol):
|
|
data = {
|
|
"id": cp_id, "model": str(model), "vendor": str(vendor),
|
|
"last_activity": time.time(), "status": "Connected", "msg_count": 0,
|
|
"boot_time_ms": None, "reconnections": 0, "clock_drift": None,
|
|
"rtt_avg_ms": 0, "subprotocol": subprotocol, "last_disconnect_code": None
|
|
}
|
|
if self.redis:
|
|
await self.redis.hset("chargepoints", cp_id, json.dumps(data))
|
|
else:
|
|
self.memory_cps[cp_id] = data
|
|
|
|
async def update_disconnect(self, cp_id, code):
|
|
if self.redis:
|
|
data = await self.redis.hget("chargepoints", cp_id)
|
|
if data:
|
|
cp = json.loads(data)
|
|
cp["last_disconnect_code"] = code
|
|
await self.redis.hset("chargepoints", cp_id, json.dumps(cp))
|
|
elif cp_id in self.memory_cps:
|
|
self.memory_cps[cp_id]["last_disconnect_code"] = code
|
|
|
|
async def update_cp_status(self, cp_id, status):
|
|
if self.redis:
|
|
data = await self.redis.hget("chargepoints", cp_id)
|
|
if data:
|
|
cp = json.loads(data)
|
|
cp["status"] = str(status)
|
|
cp["last_activity"] = time.time()
|
|
await self.redis.hset("chargepoints", cp_id, json.dumps(cp))
|
|
elif cp_id in self.memory_cps:
|
|
self.memory_cps[cp_id]["status"] = str(status)
|
|
self.memory_cps[cp_id]["last_activity"] = time.time()
|
|
|
|
async def purge_frames(self, cp_id):
|
|
if self.redis:
|
|
await self.redis.delete(f"frames:{cp_id}")
|
|
await self.redis.delete(f"stream:{cp_id}")
|
|
data = await self.redis.hget("chargepoints", cp_id)
|
|
if data:
|
|
cp = json.loads(data)
|
|
cp["msg_count"] = 0
|
|
await self.redis.hset("chargepoints", cp_id, json.dumps(cp))
|
|
elif cp_id in self.memory_frames:
|
|
self.memory_frames[cp_id].clear()
|
|
if cp_id in self.memory_cps:
|
|
self.memory_cps[cp_id]["msg_count"] = 0
|
|
|
|
async def log_frame(self, cp_id, direction, messageTypeId, action, uniqueId, payload):
|
|
now = time.time()
|
|
rtt_ms = None
|
|
schema_error = None
|
|
|
|
# Normalisation du payload en camelCase
|
|
payload = normalize_keys(payload)
|
|
|
|
if messageTypeId == 2:
|
|
self.action_map[uniqueId] = action
|
|
self.pending_calls[uniqueId] = now
|
|
elif messageTypeId == 3 and action is None:
|
|
rtt_ms = (now - self.pending_calls.pop(uniqueId, now)) * 1000
|
|
action = self.action_map.pop(uniqueId, "Unknown")
|
|
elif messageTypeId == 4 and action is None:
|
|
rtt_ms = (now - self.pending_calls.pop(uniqueId, now)) * 1000
|
|
action = self.action_map.pop(uniqueId, "Unknown")
|
|
|
|
frame = {
|
|
"timestamp": now, "direction": direction, "messageTypeId": messageTypeId,
|
|
"action": action, "uniqueId": uniqueId, "payload": payload
|
|
}
|
|
|
|
if self.redis:
|
|
stream_frame = {
|
|
"timestamp": str(now), "direction": str(direction), "messageTypeId": str(messageTypeId),
|
|
"action": str(action), "uniqueId": str(uniqueId), "payload": json.dumps(payload)
|
|
}
|
|
await self.redis.xadd(f"stream:{cp_id}", stream_frame)
|
|
|
|
await self.redis.lpush(f"frames:{cp_id}", json.dumps(frame))
|
|
await self.redis.ltrim(f"frames:{cp_id}", 0, 199)
|
|
await self.redis.expire(f"frames:{cp_id}", 86400) # TTL 24h
|
|
|
|
await self.redis.publish("live_frames", json.dumps({"cp_id": cp_id, **frame}))
|
|
|
|
data = await self.redis.hget("chargepoints", cp_id)
|
|
if data:
|
|
cp = json.loads(data)
|
|
cp["msg_count"] = cp.get("msg_count", 0) + 1
|
|
if action in ["StartTransaction", "StopTransaction"] and "timestamp" in payload:
|
|
try:
|
|
cp_time = datetime.fromisoformat(payload["timestamp"].replace("Z", "+00:00"))
|
|
cp["clock_drift"] = abs((datetime.now(timezone.utc) - cp_time).total_seconds())
|
|
except: pass
|
|
if rtt_ms: cp["rtt_avg_ms"] = int(rtt_ms)
|
|
await self.redis.hset("chargepoints", cp_id, json.dumps(cp))
|
|
else:
|
|
self.memory_frames[cp_id].append(frame)
|
|
if cp_id in self.memory_cps:
|
|
self.memory_cps[cp_id]["msg_count"] += 1
|
|
|
|
async def get_cps(self):
|
|
if self.redis:
|
|
data = await self.redis.hgetall("chargepoints")
|
|
return [json.loads(v) for v in data.values()]
|
|
return list(self.memory_cps.values())
|
|
|
|
async def get_frames(self, cp_id):
|
|
if self.redis:
|
|
data = await self.redis.lrange(f"frames:{cp_id}", 0, -1)
|
|
return [json.loads(f) for f in reversed(data)]
|
|
return list(self.memory_frames[cp_id])
|
|
|
|
session_mgr = SessionManager(redis_url="redis://redis:6379")
|