133 lines
5.4 KiB
Python
133 lines
5.4 KiB
Python
|
|
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())
|