Initial commit: Check-My-IRVE SaaS (OCPP 1.6, Audit, Diagnostics)

This commit is contained in:
Adam Belkacemi 2026-08-25 19:08:15 +02:00
commit 192be86487
44 changed files with 2070 additions and 0 deletions

5
simulator/Dockerfile Normal file
View file

@ -0,0 +1,5 @@
FROM python:3.11-slim
WORKDIR /app
COPY charge_point_sim.py .
RUN pip install websockets ocpp==2.0.0rc1
CMD ["python", "charge_point_sim.py"]

View file

@ -0,0 +1,132 @@
import asyncio
import websockets
import argparse
import random
from ocpp.routing import on
from ocpp.v16 import ChargePoint as cp
from ocpp.v16 import call
from ocpp.v16 import call_result
from ocpp.v16.enums import Action
class SimulatorChargePoint(cp):
def __init__(self, id, connection, vendor, model):
super().__init__(id, connection)
self.vendor = vendor
self.model = model
# --- Handlers pour les commandes CSMS -> Borne ---
@on(Action.GetConfiguration)
async def on_get_config(self, **kwargs):
return call_result.GetConfiguration(
configuration_key=[
{"key": "HeartbeatInterval", "value": "30", "readonly": False},
{"key": "WebSocketPingInterval", "value": "60", "readonly": False}
],
unknown_key=[]
)
@on(Action.RemoteStartTransaction)
async def on_remote_start(self, **kwargs):
return call_result.RemoteStartTransaction(status="Accepted")
@on(Action.RemoteStopTransaction)
async def on_remote_stop(self, **kwargs):
return call_result.RemoteStopTransaction(status="Accepted")
@on(Action.Reset)
async def on_reset(self, **kwargs):
return call_result.Reset(status="Accepted")
@on(Action.TriggerMessage)
async def on_trigger(self, **kwargs):
return call_result.TriggerMessage(status="Accepted")
@on(Action.ChangeConfiguration)
async def on_change_config(self, **kwargs):
return call_result.ChangeConfiguration(status="Accepted")
@on(Action.UnlockConnector)
async def on_unlock(self, **kwargs):
return call_result.UnlockConnector(status="Unlocked")
# --- Tâche de fond : Heartbeat ---
async def send_heartbeat(self):
while True:
await asyncio.sleep(30)
try:
await self.call(call.Heartbeat())
except websockets.exceptions.ConnectionClosed:
break
# --- Cycle de vie continu (Voitures qui arrivent et repartent) ---
async def run_lifecycle(self):
# 1. Boot initial
await self.call(call.BootNotification(charge_point_vendor=self.vendor, charge_point_model=self.model))
await self.call(call.StatusNotification(connector_id=1, error_code="NoError", status="Available"))
print(f"[{self.id}] Borne en ligne. En attente de véhicules...")
while True:
# Temps d'attente avant l'arrivée d'une voiture (5 à 15 secondes)
wait_time = random.randint(5, 15)
await asyncio.sleep(wait_time)
# 2. Arrivée d'un véhicule
await self.call(call.StatusNotification(connector_id=1, error_code="NoError", status="Preparing"))
auth = await self.call(call.Authorize(id_tag="CHARGEMAP_PASS"))
if auth.id_tag_info["status"] == "Accepted":
tx = await self.call(call.StartTransaction(
connector_id=1, id_tag="CHARGEMAP_PASS", meter_start=0,
timestamp="2026-08-11T14:00:00Z"
))
await self.call(call.StatusNotification(connector_id=1, error_code="NoError", status="Charging"))
# 3. Cycle de charge (3 à 6 envois de MeterValues)
consumption = 0
charge_cycles = random.randint(3, 6)
for _ in range(charge_cycles):
consumption += random.randint(500, 1500)
await self.call(call.MeterValues(
connector_id=1,
transaction_id=tx.transaction_id,
meter_value=[{"timestamp": "2026-08-11T14:00:00Z", "sampled_value": [{"value": str(consumption)}]}]
))
await asyncio.sleep(3) # Recharge de 3s
# 4. Départ du véhicule
await self.call(call.StopTransaction(
transaction_id=tx.transaction_id, meter_stop=consumption,
timestamp="2026-08-11T14:01:00Z"
))
await self.call(call.StatusNotification(connector_id=1, error_code="NoError", status="Finishing"))
await asyncio.sleep(2)
await self.call(call.StatusNotification(connector_id=1, error_code="NoError", status="Available"))
else:
# Badge refusé
await self.call(call.StatusNotification(connector_id=1, error_code="NoError", status="SuspendedEV"))
await asyncio.sleep(5)
await self.call(call.StatusNotification(connector_id=1, error_code="NoError", status="Available"))
async def main():
parser = argparse.ArgumentParser()
parser.add_argument("--url", default="ws://backend:8000/ocpp")
parser.add_argument("--cp-id", default="SIM-001")
parser.add_argument("--vendor", default="Tesla")
parser.add_argument("--model", default="Model3")
args = parser.parse_args()
full_url = f"{args.url}/{args.cp_id}"
print(f"Connexion à {full_url}...")
async with websockets.connect(full_url, subprotocols=["ocpp1.6"]) as ws:
cp = SimulatorChargePoint(args.cp_id, ws, args.vendor, args.model)
# On lance l'écoute CSMS, les Heartbeats et le cycle de vie en parallèle
await asyncio.gather(
cp.start(),
cp.send_heartbeat(),
cp.run_lifecycle()
)
if __name__ == "__main__":
asyncio.run(main())