Connect your chargers with Django
A Django app you mount with one include() — models, endpoints and hub client ready-made.
Works with Django ≥ 4.2 · Python ≥ 3.10
https://sajilocharge.com/developers/ocpi/ai/ —
this whole tutorial in machine-readable form.
How a roaming charge flows
Sajilo Charge driver Sajilo Charge hub Your Django backend
│ taps "Start" ────▶ POST /commands/START_SESSION ──▶ onStartSession(charge)
│ your CSMS starts the charger
│ charging… ◀── PUT sessions push ◀───────────── setActive() / meter(kwh)
│ taps "Stop" ────▶ POST /commands/STOP_SESSION ───▶ onStopSession(charge)
│ wallet debited ◀── POST cdrs ◀────────────────────── complete(kwh, cost)
│ monthly netting ─────────────────▶ you get paid1.Install the SDK
One download, one package-manager command.
curl -LO https://sajilocharge.com/static/downloads/sajilocharge-ocpi-django.zip
pip install ./sajilocharge-ocpi-django.zip
2.Configure and mount
Tell the SDK who you are and where your server is publicly reachable. This mounts the three /ocpi endpoints the hub calls on you — everything else is outbound from your side.
# settings.py
INSTALLED_APPS += ['sajilocharge_ocpi']
SAJILOCHARGE_OCPI = {
'PARTY_ID': 'ABC', # your 3-char party id
'COUNTRY_CODE': 'NP',
'NAME': 'ABC Charging Pvt. Ltd.',
'BASE_URL': 'https://csms.example.com', # public URL of this app
'HANDLERS': 'myapp.ocpi.Handlers',
}
# urls.py
urlpatterns += [path('ocpi/', include('sajilocharge_ocpi.urls'))]
# then:
# python manage.py migrate
3.Wire your two charger callbacks
Every remote-start request becomes a RoamingCharge handed to your code. You implement exactly two methods; keep them fast and do the charger work asynchronously.
# myapp/ocpi.py — the only file you write
from sajilocharge_ocpi.handlers import BaseHandlers
class Handlers(BaseHandlers):
def on_start_session(self, charge):
# charge.location_id / charge.evse_uid say which charger
my_csms.remote_start(charge.evse_uid, ref=str(charge.id))
return True # accepted; do the real work async
def on_stop_session(self, charge):
my_csms.remote_stop(ref=str(charge.id))
return True
4.Drive the charge lifecycle
From your CSMS events, three calls push the matching OCPI Session objects — and the final CDR that bills the driver — to the hub. The CDR is idempotent: it goes out exactly once per charge.
from sajilocharge_ocpi.models import RoamingCharge
charge = RoamingCharge.objects.get(pk=ref)
charge.set_active() # energy started flowing
charge.meter(kwh=4.2) # periodic session kWh
charge.complete(kwh=18.5, total_cost=462.50) # NPR; issues the CDR
5.Join the network
Request a one-shot registration token from partners@sajilocharge.com. Deploy first — the hub calls your /ocpi endpoints back during the handshake — then run the join. Tokens rotate on every re-registration; the registration token dies on first use.
python manage.py sajilocharge_join --token <registration-token>
python manage.py sajilocharge_status # → Status: ACTIVE
6.Publish your stations
Locations upsert by id — re-push whenever anything changes, keep live EVSE status fresh, and (optionally) verify drivers in real time for RFID or local starts.
from sajilocharge_ocpi.client import HubClient
HubClient().push_location({
'id': 'LOC-KTM-01',
'name': 'ABC Hub Thamel',
'address': 'Thamel Marg 12',
'city': 'Kathmandu',
'coordinates': {'latitude': '27.7154', 'longitude': '85.3123'},
'publish': True,
'evses': [{'uid': 'EVSE-01', 'status': 'AVAILABLE',
'connectors': [{'id': '1', 'standard': 'IEC_62196_T2',
'format': 'SOCKET', 'power_type': 'AC_3_PHASE'}]}],
})
# live availability + optional driver check
HubClient().update_evse('LOC-KTM-01', 'EVSE-01', {'status': 'CHARGING'})
HubClient().authorize(token_uid) # ALLOWED | NO_CREDIT | BLOCKED
Money and settlement
- All amounts are NPR; costs are VAT-exclusive.
- Each CDR credits your inter-party ledger at the hub; settlement is monthly netting minus the hub clearing fee.
- Disputes: the hub's CDR dispute endpoint or partners@sajilocharge.com — open disputes pause netting for that CDR.
Troubleshooting
- Join fails with "could not fetch endpoints" — your base URL isn't publicly reachable, or the OCPI routes aren't mounted. The hub must be able to call you back during the handshake.
- 401 on hub calls — the connection was re-registered somewhere else; join again with a fresh registration token.
- Session push returns "No matching session" — push only charges the hub initiated, after START_SESSION was accepted.
- Rate limits — the hub throttles per-party (default 300 req/min); meter updates every 30–60 s are plenty.
Other stacks
Laravel (Laravel 10 / 11 / 12 · PHP ≥ 8.1) · Node.js (Node ≥ 18 · Express ≥ 4) · Spring Boot (Spring Boot 3.2+ · Java 17+)
Sajilo Charge Django SDK v0.1.0 · OCPI 2.2.1 · Questions: partners@sajilocharge.com