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
5
simulator/Dockerfile
Normal file
5
simulator/Dockerfile
Normal 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"]
|
||||
132
simulator/charge_point_sim.py
Normal file
132
simulator/charge_point_sim.py
Normal 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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue