CitrineOS Integration¶
This example demonstrates Opti-VGI integrated with CitrineOS, an open-source OCPP 1.6J-compliant Charging Station Management System (CSMS). The demo runs a multi-service Docker stack that simulates six EV charging stations connecting to CitrineOS, with Opti-VGI performing smart charging optimization in real time.
The key concept illustrated is curtailment: when aggregate EV demand exceeds the site power limit, the optimizer must reduce individual charging allocations. In the default scenario, six EVs each request 7.2 kW (43.2 kW total), but the site limit is only 30 kW. Opti-VGI’s scheduling algorithm distributes available power fairly across all connected vehicles while respecting each EV’s minimum and maximum charge rates.
See Architecture Overview for details on the core Opti-VGI scheduling framework.
Components¶
docker-compose.yml– Multi-service stack definition (CitrineOS, PostgreSQL, RabbitMQ, Hasura, Opti-VGI, Simulator)src/translation/citrineos.py–CitrineOSTranslationclass implementingget_evsandsend_power_to_evsfor the CitrineOS REST APIsrc/app.py– Entry point withLoggingTranslationwrapper and RabbitMQ listener for new-session triggerssimulator/– OCPP 1.6J charger simulator that creates six virtual charging stationssimulator/scenarios/default.json– Default demo scenario configuration (staggered EV arrivals)example.env– Environment variable configuration (copy to.envbefore running)verify.py– Automated curtailment verification script (checks profiles, sessions, aggregate power, curtailment)
Running the Demo¶
Prerequisites:
Docker and Docker Compose (v2)
Steps:
Navigate to the example directory:
cd examples/citrineos
Copy the example environment file:
cp example.env .env
Build and start the stack:
docker compose up --build --attach opti-vgi --attach simulator
The --attach flags show output from only the Opti-VGI optimizer and the charger simulator,
keeping infrastructure services (PostgreSQL, RabbitMQ, CitrineOS, Hasura) silent. Services
start in dependency order automatically. EVs arrive at staggered 60-second intervals, and log
output shows arrivals, departures, and curtailment events when aggregate demand exceeds the
site limit.
Press Ctrl+C to stop all services.
Demo Scenario¶
The default scenario (scenarios/default.json) simulates six EVs arriving at staggered
60-second intervals on six separate charging stations. Each EV has a maximum charge rate of
7.2 kW. With a site power limit of 30 kW, curtailment activates once enough EVs are
connected that their combined demand exceeds the limit.
For example, when the first four EVs are connected, aggregate demand is 4 x 7.2 = 28.8 kW, which is under the 30 kW limit – no curtailment needed. Once the fifth EV arrives (5 x 7.2 = 36.0 kW requested), the optimizer must reduce allocations. With all six EVs connected (6 x 7.2 = 43.2 kW requested), each EV receives approximately 5.0 kW to stay within the 30 kW site limit.
How Curtailment Works¶
Opti-VGI runs a scheduling loop that is triggered by new EV session events arriving via RabbitMQ. On each cycle, the optimizer:
Queries CitrineOS for all active charging transactions
Reads each EV’s energy needs, min/max power, and timing constraints
Runs the
GoAlgorithmoptimization to allocate power within the site limitSends a
SetChargingProfilecommand to CitrineOS for each EV with its allocated power
The following diagrams show the service topology and the scheduling data flow.
graph TD
simulator["Charger Simulator<br/>(6 OCPP stations)"]
citrineos["CitrineOS CSMS<br/>(v1.8.3)"]
optivgi["Opti-VGI<br/>(Smart Charging)"]
db["PostgreSQL"]
rabbitmq["RabbitMQ"]
hasura["Hasura GraphQL"]
simulator -->|"OCPP 1.6J<br/>WebSocket"| citrineos
citrineos --> db
citrineos -->|"Transaction events"| rabbitmq
rabbitmq -->|"New session triggers"| optivgi
optivgi -->|"REST API<br/>get_evs / SetChargingProfile"| citrineos
optivgi -->|"GraphQL<br/>MeterValues"| hasura
hasura --> db
CitrineOS Demo Service Topology¶
sequenceDiagram
participant Sim as Simulator
participant COS as CitrineOS
participant RMQ as RabbitMQ
participant OV as Opti-VGI
Sim->>COS: BootNotification
COS-->>Sim: Accepted
Sim->>COS: StartTransaction
COS->>RMQ: Transaction event
RMQ->>OV: New session trigger
OV->>COS: GET active transactions
COS-->>OV: EV data
OV->>OV: Run optimization (GoAlgorithm)
OV->>COS: SetChargingProfile (per EV)
COS-->>OV: Accepted
Scheduling Flow¶
Environment Variables¶
All configuration is done through environment variables defined in example.env.
Copy it to .env and modify as needed.
Service Ports
Variable |
Default |
Description |
|---|---|---|
|
|
CitrineOS REST API port exposed to host |
|
|
OCPP 1.6J WebSocket port exposed to host |
|
|
Hasura GraphQL Engine port exposed to host |
|
|
Operator dashboard port (if enabled) |
Simulator Configuration
Variable |
Default |
Description |
|---|---|---|
|
6 stations |
Comma-separated station identifiers (e.g., |
|
|
Number of OCPP connectors per station |
|
|
Interval in seconds between MeterValues messages |
|
|
WebSocket URL for OCPP connection (internal Docker network) |
|
|
Scenario file path; set to empty string for manual trigger mode |
|
6 tags |
Comma-separated idTags to seed in CitrineOS Authorizations table |
Opti-VGI Configuration
Variable |
Default |
Description |
|---|---|---|
|
|
Site-wide power limit in kW for curtailment |
|
|
Voltage (V) for watt/ampere conversions |
|
|
Station group names for Opti-VGI scheduling |
Internal URLs
Variable |
Default |
Description |
|---|---|---|
|
|
CitrineOS REST API URL (internal Docker network) |
|
|
Hasura GraphQL URL (internal Docker network) |
|
|
RabbitMQ AMQP connection URL |
Per-Connector EV Configuration
Each connector is configured with a set of CONN_XX_* variables where XX is a
zero-padded index (01 through 06 in the default setup). Each CONN_XX maps to the
Nth station in STATION_IDS.
Variable Pattern |
Example |
Description |
|---|---|---|
|
|
Energy needed by the EV in kWh |
|
|
Maximum charge rate in kW |
|
|
Minimum charge rate in kW |
|
|
Simulated arrival time (HH:MM) |
|
|
Simulated departure time (HH:MM) |
Operator Dashboard¶
While the demo is running, open the CitrineOS operator UI to see charging stations, active transactions, and charging profiles in real time.
URL:
http://localhost:3000(or yourOPERATOR_UI_PORT)Username:
admin@citrineos.comPassword:
CitrineOS!
The Hasura GraphQL console is also available at http://localhost:8090 (or your
HASURA_PORT) for direct database queries (e.g., browsing the Transactions or
ChargingProfiles tables).
Expected Output¶
When the demo is running, the logs show only meaningful events — EV arrivals, departures, and curtailment transitions. Representative output:
[Opti-VGI] INFO EV 1 arrived — max 7.20 kW
[Opti-VGI] INFO OCPP event: StartTransaction
[Opti-VGI] INFO EV 2 arrived — max 7.20 kW
[Opti-VGI] INFO EV 3 arrived — max 7.20 kW
[Opti-VGI] INFO EV 4 arrived — max 7.20 kW
[Opti-VGI] INFO EV 5 arrived — max 7.20 kW
[Opti-VGI] INFO CURTAILMENT ON — 5 EVs requesting 36.0 kW, site limit 30.0 kW, allocated 30.0 kW
[Opti-VGI] INFO EV 1: 7.00 kW / 7.20 kW max
[Opti-VGI] INFO EV 2: 7.20 kW / 7.20 kW max
[Opti-VGI] INFO EV 3: 7.20 kW / 7.20 kW max
[Opti-VGI] INFO EV 4: 1.40 kW / 7.20 kW max
[Opti-VGI] INFO EV 5: 7.20 kW / 7.20 kW max
You can verify that curtailment is working correctly by running the automated verification script after the demo has run for a few minutes:
python verify.py
Representative output:
=== Opti-VGI Curtailment Verification ===
Config: url=http://localhost:8090, site_limit=30.0 kW, voltage=240.0 V
[PASS] Check 1: Charging profiles sent -- Found 6 charging profiles (need >= 3)
[PASS] Check 2: Concurrent sessions -- Found 6 profiles with active allocations (need >= 3)
[PASS] Check 3: Aggregate within limit -- Aggregate in latest cycle (6 EVs, ...): 30.0 kW <= 30.0 kW limit (+0.8 kW rounding tolerance)
[PASS] Check 4: Curtailment occurred -- Found 60 curtailed profiles in history (min was 1.44 kW, max rate 7.20 kW)
Result: 4/4 checks passed -- PASS
Add --plot to generate a stacked area chart showing per-EV power allocation over
time, with the site limit line and over-limit periods highlighted in red:
python verify.py --plot
This saves curtailment_plot.png in the current directory. The plot shows each EV’s
allocated power as a colored band, making it easy to see how the optimizer redistributes
power when curtailment is active.
Customization¶
You can modify the demo behavior by changing environment variables in your .env file:
``SITE_POWER_LIMIT_KW`` – Increase or decrease the site limit to see more or less curtailment. For example, setting it to
50eliminates curtailment with six 7.2 kW EVs, while20forces more aggressive reduction.``CONN_XX_MAX_POWER_KW`` – Change individual EV max charge rates to create heterogeneous fleets.
``SCENARIO_FILE`` – Set to an empty string (
SCENARIO_FILE=) to disable automatic EV arrivals and use manual trigger mode instead.``STATION_IDS`` – Add or remove station IDs to change the number of simulated chargers. Remember to add corresponding
CONN_XX_*variables for new stations.
Troubleshooting¶
Issue |
Solution |
|---|---|
Port 8080 already in use |
Change |
CitrineOS takes 60+ seconds to start |
This is normal on first run due to database migrations. The health check has a 60-second |
Docker memory issues with 8+ containers |
Allocate at least 4 GB of memory to Docker Desktop (Settings > Resources) |
Simulator connects before CitrineOS is ready |
This is handled automatically by Docker health checks and |
|
The docker-compose configuration patches this automatically via an entrypoint |