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())